Problemi Daraltmak: Binary Search, git bisect run ve Delta Debugging
Hatayı bulmanın en hızlı yolu aralığı her adımda ikiye bölmektir: kodda, katmanlarda, commit'lerde ve girdide. git bisect run 31 commit'lik aralıkta derlenmeyen commit'i 125 ile atlayıp hatalı refactoring'i 7 denemede buluyor; ddmin algoritması 5.000 satırlık import dosyasını 3 satıra indiriyor.
Sürüm notu: Bu yazıdaki kodlar .NET 10 ile derlenip denenmiştir; git bisect örnekleri Git 2.54 ile çalıştırılmıştır.
Önce Tahmin Et
Elinde iki sürüm var: v1.8.0'da sipariş toplamı doğru, v1.10.0'da #10482 numaralı sipariş 1 kuruş eksik hesaplanıyor. İki tag arasında 30 commit var, v1.8.0'ın kendisiyle birlikte 31. Bu commit'lerden biri yarım kalmış bir iş ve derlenmiyor. Hatayı getiren commit'i git bisect run ile otomatik arıyorsun. Script kaç kez çalışır?
İkinci soru farklı bir bug'dan: katalog import'u 5.000 satırlık bir CSV (Comma-Separated Values) dosyasında exception fırlatıyor. Dosyayı otomatik küçülten bir algoritma, hatayı hâlâ üreten kaç satırlık bir dosyaya iner ve bunun için import'u kaç kez çalıştırır? İki sorunun cevabı da yazıdaki gerçek koşuların çıktısında.
Daraltmak: Her Yerde Binary Search
Agans'ın dördüncü kuralı "Divide and Conquer": hatayı içeren aralığı her adımda ikiye böl. 1.024 elemanlı bir aralıkta tek tek bakarak arama en kötü durumda 1.024 adım sürer, ikiye bölerek arama 10 adım. Debugging'deki "aralık" ise her zaman bir dizi değildir. Kod satırları, katmanlar, commit'ler, girdi satırları ve configuration anahtarları da birer aralıktır ve hepsinde aynı fikir çalışır: ortadan bir ölçüm al, hatanın hangi yarıda olduğunu öğren, diğer yarıyı unut.
Kodda bunun en basit hali, akışın ortasına bir log satırı ya da bir Debug.Assert koymaktır. Sipariş toplamı yanlışsa ve hesap beş adımdan geçiyorsa, üçüncü adımın çıktısını yazdır. Değer orada doğruysa hata dördüncü ya da beşinci adımda, yanlışsa ilk üçte. Bir başka yol devre dışı bırakmaktır: indirim hesabını geçici olarak kapat, fark kayboluyor mu?
Katmanlı bir sistemde aynı soru katman sınırlarında sorulur. Bookstore'da kullanıcı toplamı Angular ekranında görüyor; değer bir API (Application Programming Interface) çağrısından geliyor, API de veritabanına yazıyor. Tarayıcının Network panelinde response'taki total alanı 178,46 ise Angular'ı aramaktan vazgeç, hata backend'de. API'nin hesapladığı değer ile veritabanına yazılan değer aynıysa, veritabanını da bırak. Bizim vakada üç ölçüm de 178,46 gösteriyor ve hata, önceki iki yazıda gördüğümüz gibi API'nin içindeki satır hesabında.
Ama "hangi satırda" sorusu cevaplandıktan sonra bile cevapsız bir soru kalıyor: muhasebe bu farkı önceki haftaların mutabakatlarında hiç görmemişti. Hata ne zaman, hangi değişiklikle geldi? Bunu bilmek iki açıdan değerli. Değişikliği yapan kişi ve PR (pull request) bize niyeti anlatır: belki yuvarlama kuralı bilerek değiştirildi ve fatura tarafı güncellenmeyi unuttu. Ayrıca aynı commit'in başka neyi bozduğunu görmemizi sağlar.
git bisect run ile Commit'i Otomatik Bulmak
git bisect'in elle kullanımını, yani her adımda kodu deneyip git bisect good ya da git bisect bad yazmayı, Git serisinin "Git bisect ve git worktree: Hatayı Bulan Commit ve Aynı Anda İki Branch" yazısında anlatmıştık. Burada işi bir script'e devrediyoruz. git bisect run, her adımda verdiğin komutu çalıştırır ve çıkış koduna göre commit'i işaretler. Git dokümanına göre kural şu: 0 iyi, 1 ile 127 arası (125 hariç) kötü, 125 "bu commit test edilemiyor, atla". Bunların dışındaki bir kod, örneğin 255, bisect'i tamamen durdurur.
Önce testi yapan küçük bir program. Bisect sırasında repository'deki kod her adımda değişeceği için, kontrol programını repository'nin dışında tutuyoruz; Git dokümanı da script'lerin repository dışında olmasını daha güvenli buluyor. Program, repository'deki Bookstore.Ordering projesine path üzerinden referans veriyor ve sipariş #10482'yi her commit'in kendi koduyla hesaplıyor:
using Bookstore.Ordering;
// ticket #4127'deki #10482 numaralı sipariş; faturada 178.47 yazıyor
List<OrderLine> lines =
[
new("978-605-0000-01-1", 12.45m, 1, 0.10m),
new("978-605-0000-02-8", 89.90m, 1, 0.00m),
new("978-605-0000-03-5", 45.50m, 2, 0.15m),
];
decimal total = new OrderService().CalculateTotal(lines);
Console.WriteLine(FormattableString.Invariant($"total = {total}"));
return total == 178.47m ? 0 : 1;
OrderService.CalculateTotal, bu aralıktaki her commit'te var olan public API. Probe'u ona bağlamak bilinçli bir seçim: iç yapısı değişen bir class'a (örneğin sonradan eklenen PriceCalculator'a) bağlansaydık, eski commit'lerde derlenmezdi. Şimdi de bisect'in çağıracağı script:
#!/bin/sh
# exit 0 = good, 125 hariç 1-127 = bad, 125 = skip (bu commit test edilemiyor)
cd "$(dirname "$0")"
dotnet build Probe -v q -nologo > /dev/null 2>&1 || exit 125
dotnet run --project Probe --no-build
Build başarısızsa 125 dönüyoruz. Bu önemli: derlenmeyen bir commit "kötü" sayılırsa bisect hatayı yanlış commit'e yıkar. Bir commit'in test edilemediğini söylemek, onun hakkında yalan söylemekten iyidir. Bisect'i başlatıp script'i veriyoruz:
$ git bisect start v1.10.0 v1.8.0
Bisecting: 14 revisions left to test after this (roughly 4 steps)
[d70e8e016db0eace38f03bc605508256f9f0e36d] WIP installments
$ git bisect run ../bisect/order-total.sh
running '../bisect/order-total.sh'
Bisecting: 14 revisions left to test after this (roughly 4 steps)
[5d956711e7e4d6143640bbdd7182623e9f024aed] Add inventory reservation
running '../bisect/order-total.sh'
total = 178.47
Bisecting: 13 revisions left to test after this (roughly 4 steps)
[dc3e1c229c3d6c5d98b24de4564a60450e8da824] Fix installments build
running '../bisect/order-total.sh'
total = 178.46
Bisecting: 6 revisions left to test after this (roughly 3 steps)
[d2e87b0da22167f351c661e35b6bb93b0d3e08f0] Docs: ordering overview
running '../bisect/order-total.sh'
total = 178.47
Bisecting: 3 revisions left to test after this (roughly 2 steps)
[6b6e917b554584173ee2b3f387b13930ac0f3598] Extract PriceCalculator from OrderService
running '../bisect/order-total.sh'
total = 178.46
Bisecting: 1 revision left to test after this (roughly 1 step)
[8ea4570ea40f696f8d220e947877e9d436adf12a] Inventory: warehouse list
running '../bisect/order-total.sh'
total = 178.47
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[dfdd6eed04341206cb6d3363664a56fc603e09bf] Customer: address validation
running '../bisect/order-total.sh'
total = 178.47
6b6e917b554584173ee2b3f387b13930ac0f3598 is the first bad commit
commit 6b6e917b554584173ee2b3f387b13930ac0f3598
Author: Linus <linus@example.com>
Date: Sat Sep 5 14:00:00 2026 +0300
Extract PriceCalculator from OrderService
src/Bookstore.Ordering/OrderService.cs | 11 +----------
src/Bookstore.Ordering/PriceCalculator.cs | 9 +++++++++
2 files changed, 10 insertions(+), 10 deletions(-)
create mode 100644 src/Bookstore.Ordering/PriceCalculator.cs
bisect found first bad commit
İlk sorunun cevabı: script 7 kez çalıştı ve 8 saniyede ilk kötü commit'i buldu. İlk durak tam ortadaki commit'ti ve şans eseri derlenmeyen WIP installments commit'iydi; script 125 döndü ve bisect onu atlayıp başka bir commit seçti. Ardından aralık her adımda yarıya indi ve sonunda tek commit kaldı. Adımlar şöyle görünüyor:

Bulunan commit'in diff'i tek bakışta her şeyi anlatıyor:
diff --git a/src/Bookstore.Ordering/OrderService.cs b/src/Bookstore.Ordering/OrderService.cs
index 362be69..d333a71 100644
--- a/src/Bookstore.Ordering/OrderService.cs
+++ b/src/Bookstore.Ordering/OrderService.cs
@@ -4,14 +4,5 @@ public sealed record OrderLine(string Isbn, decimal UnitPrice, int Quantity, dec
public sealed class OrderService
{
- public decimal CalculateTotal(IReadOnlyList<OrderLine> lines)
- {
- decimal total = 0;
- foreach (OrderLine line in lines)
- {
- decimal unit = Math.Round(line.UnitPrice * (1 - line.DiscountRate), 2, MidpointRounding.AwayFromZero);
- total += unit * line.Quantity;
- }
- return total;
- }
+ public decimal CalculateTotal(IReadOnlyList<OrderLine> lines) => lines.Sum(PriceCalculator.LineTotal);
}
Diff'in ikinci yarısı olan yeni PriceCalculator.cs dosyasında aynı satır Math.Round(line.UnitPrice * (1 - line.DiscountRate), 2) olarak duruyor; MidpointRounding.AwayFromZero argümanı taşınırken düşmüş. Commit mesajı "Extract PriceCalculator", yani davranışı değiştirmemesi gereken bir refactoring. Kimse yuvarlama kuralını bilerek değiştirmemiş; bir argüman sessizce kaybolmuş ve derleyici bunu fark edemezdi, çünkü iki overload da geçerli. Bu ayrıntı, bu serinin 11 numaralı yazısında yapacağımız root cause analizinin ana girdilerinden biri olacak.
Bisect bittiğinde git bisect reset ile başladığın branch'e dön. Oturumu git bisect log > bisect.log ile kaydetmek de iyi bir alışkanlık: bir adımı yanlış işaretlediğini fark edersen, dosyadan o satırı silip git bisect replay bisect.log ile oturumu doğru haliyle tekrar oynatabilirsin. Bu kayıt, Agans'ın "audit trail" kuralının bisect'teki karşılığı.
Flaky Testle Bisect'in Tehlikesi
Bisect, her adımın cevabına körü körüne güvenir. Tek bir yanlış "good" ya da "bad", aramayı yanlış yarıya gönderir ve bisect sonunda tam bir özgüvenle yanlış commit'i gösterir. Bu yüzden bisect script'indeki test deterministik olmalı. Bizim kontrolümüz saf bir hesap, deterministik. Ama ağa çıkan, zamana bağlı ya da thread sırasına duyarlı bir test kullanıyorsan durum farklı.
Hesap basit. Kötü commit'lerde testin yalnız %20 ihtimalle kırmızı olduğunu düşün. Tek koşuda kötü bir commit'i iyi sanma ihtimalin %80. Beş koşuda bu ihtimal 0,8 üzeri 5, yani yaklaşık %33; yirmi koşuda yaklaşık %1,2. Yani aşağıdaki gibi bir script işe yarar, ama tekrar sayısını testin ne kadar flaky olduğuna göre seçmelisin:
#!/bin/sh
# flaky bir kontrol için: beş koşudan biri kırmızıysa commit bad sayılır
cd "$(dirname "$0")"
dotnet build Probe -v q -nologo > /dev/null 2>&1 || exit 125
for i in 1 2 3 4 5; do
dotnet run --project Probe --no-build > /dev/null || exit 1
done
exit 0
Bu script'le aynı bisect'i tekrar çalıştırdığımda yine 7 adımda aynı commit'i, 6b6e917'yi buldu; doğal olarak, çünkü testimiz zaten deterministik. Asıl çözüm, flaky testi bisect'e sokmadan önce deterministik hale getirmektir: zamanı TimeProvider ile, rastgeleliği seed ile, dış servisi bir fake ile sabitlemek.
İki pratik not daha. Birincisi, merge commit'lerinin yoğun olduğu bir geçmişte git bisect start --first-parent, aramayı yalnız ana branch'e giren merge'ler üzerinde yapar; önce hangi PR'ın hatayı getirdiğini bulup sonra o PR'ın içine inebilirsin. İkincisi, adım başına süre önemli: bizim adımımız 1 saniye civarında; gerçek bir solution'da build ve test adım başına birkaç dakika sürebilir. 30 commit için bu yine de 5 adım demek, ama 5.000 commit için 13 adım; elle bakmakla kıyaslanamaz.
Delta Debugging: Girdiyi Otomatik Küçültmek
Bisect, değişiklik geçmişini daraltır. Bazen daraltılması gereken şey girdi: bug belli bir dosyada, request'te ya da veri setinde ortaya çıkıyor ve o girdi devasa. Bookstore'un katalog import'u, tedarikçiden gelen bir CSV dosyasında patlıyor:
Unhandled exception. System.ArgumentException: An item with the same key has already been added. Key: 9786051012077
at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
at Bookstore.Catalog.BookImporter.Import(IReadOnlyList`1 lines) in ./BookImporter.cs:line 20
at Program.<Main>$(String[] args) in ./Program.cs:line 4
Import kodu şöyle:
namespace Bookstore.Catalog;
public sealed record ImportedBook(string Isbn, string Title, decimal Price, int Stock);
public static class BookImporter
{
public static Dictionary<string, ImportedBook> Import(IReadOnlyList<string> lines)
{
string[] header = lines[0].Split(',');
int isbnCol = Array.IndexOf(header, "isbn");
int titleCol = Array.IndexOf(header, "title");
int priceCol = Array.IndexOf(header, "price");
int stockCol = Array.IndexOf(header, "stock");
Dictionary<string, ImportedBook> books = [];
foreach (string line in lines.Skip(1))
{
string[] cells = line.Split(',');
string isbn = cells[isbnCol].Replace("-", "");
books.Add(isbn, new ImportedBook(isbn, cells[titleCol],
decimal.Parse(cells[priceCol], System.Globalization.CultureInfo.InvariantCulture),
int.Parse(cells[stockCol])));
}
return books;
}
}
Hata mesajı bir key'i söylüyor ama dosyada bu key tire'siz haliyle geçmiyor olabilir; ayrıca tek başına hangi satırların bu hatayı ürettiğini söylemiyor. 5.000 satırı elle yarıya bölmek bir yöntem, ama hata iki satırın birlikte bulunmasına bağlıysa basit ikiye bölme işe yaramaz: iki yarının hiçbiri tek başına hatayı üretmez. Andreas Zeller ve Ralf Hildebrandt'ın 2002'de IEEE'nin (Institute of Electrical and Electronics Engineers) Transactions on Software Engineering dergisinde yayımlanan "Simplifying and Isolating Failure-Inducing Input" makalesindeki ddmin algoritması bu durumu da çözer. Algoritma girdiyi n parçaya böler; önce her parçayı tek başına, sonra her parçanın tümleyenini (o parça hariç geri kalanını) dener. Hata devam eden en küçük adayı alır; hiçbiri hatayı üretmezse parçaları küçültüp (n'i iki katına çıkarıp) tekrar dener. Sonuç 1-minimal'dir: kalan satırlardan herhangi birini çıkarırsan hata kaybolur.
namespace Bookstore.Tools;
public static class DeltaDebugger
{
// Zeller'in ddmin'i: girdinin hâlâ hata veren 1-minimal alt kümesini döner
public static List<T> Minimize<T>(List<T> input, Func<List<T>, bool> fails)
{
List<T> current = input;
int n = 2;
while (current.Count >= 2)
{
List<List<T>> chunks = Split(current, n);
bool reduced = false;
foreach (List<T> chunk in chunks)
{
if (fails(chunk))
{
current = chunk;
n = 2;
reduced = true;
break;
}
}
if (!reduced)
{
for (int i = 0; i < chunks.Count; i++)
{
List<T> complement = chunks.Where((_, j) => j != i).SelectMany(c => c).ToList();
if (fails(complement))
{
current = complement;
n = Math.Max(n - 1, 2);
reduced = true;
break;
}
}
}
if (!reduced)
{
if (n >= current.Count) break;
n = Math.Min(n * 2, current.Count);
}
}
return current;
}
private static List<List<T>> Split<T>(List<T> items, int n)
{
List<List<T>> result = [];
int start = 0;
for (int i = 0; i < n; i++)
{
int size = (items.Count - start) / (n - i);
result.Add(items.GetRange(start, size));
start += size;
}
return result;
}
}
Algoritmanın en kritik parçası, "hata devam ediyor mu?" sorusunu cevaplayan predicate. Burada bilinçli olarak dar tutuyoruz: yalnız aynı hata, yani aynı mesajla başlayan ArgumentException, "fails" sayılır. Başka herhangi bir exception, aradığımız bug değildir:
using Bookstore.Catalog;
using Bookstore.Tools;
List<string> lines = File.ReadAllLines("books-import.csv").ToList();
int tests = 0;
bool Fails(List<string> candidate)
{
tests++;
try
{
BookImporter.Import(candidate);
return false;
}
catch (ArgumentException ex) when (ex.Message.StartsWith("An item with the same key"))
{
return true; // aradığımız hatanın ta kendisi
}
catch (Exception)
{
return false; // başka bir hata sayılmaz
}
}
Console.WriteLine($"input lines: {lines.Count}, fails: {Fails(lines)}");
List<string> minimal = DeltaDebugger.Minimize(lines, Fails);
Console.WriteLine($"minimal lines: {minimal.Count}, tests run: {tests}");
minimal.ForEach(Console.WriteLine);
input lines: 5000, fails: True
minimal lines: 3, tests run: 291
isbn,title,price,stock
978-605-1012-07-7,Clean Pages 1207,531.00,40
9786051012077,Quiet Compiler 4388,707.00,34
İkinci sorunun cevabı: 5.000 satır 3 satıra indi ve bunun için import 291 kez çalıştı. Kalan üç satır sebebi kendisi anlatıyor: header satırı ve aynı ISBN'in (International Standard Book Number) iki farklı yazılışı. Satır 1.208'deki 978-605-1012-07-7 ile satır 4.389'daki 9786051012077, tire'ler silindikten sonra aynı key'e dönüşüyor. 3.000'den fazla satır aralıklı bu iki satırı gözle bulmak, dosyayı bir editörde aşağı kaydırarak neredeyse imkânsızdı. 291 çalıştırma çok gibi görünüyor ama her biri milisaniyeler sürüyor; toplam süre bir saniyenin altında.
Bu minimal girdi artık doğrudan bir regression test'e dönüşebilir: üç satırlık bir CSV ile import'un ne yapması gerektiğini (hata mı vermeli, ikinci satırı birleştirmeli mi, uyarı mı yazmalı) test eden bir xUnit testi. Hangi davranışın doğru olduğu bir iş kararı; ama artık kararın ne hakkında olduğu net.
Configuration Bisect: Ortam Farkını Daraltmak
Aynı fikir configuration'a da uygulanır. Bir davranış staging'de var, production'da yok ve iki ortamın appsettings dosyaları ile environment variable'ları arasında 40 fark var diyelim. 40 farkın yarısını, yani 20'sini staging'e uygula: hata kayboldu mu? Kaybolduysa sebep o 20'nin içinde; kaybolmadıysa diğer 20'de. Yaklaşık 6 adımda tek bir anahtara inersin. Feature flag'ler için de durum aynı: önce yarısını kapat, sonra kalan yarının yarısını.
Tek bir anahtara inemiyorsan, yani hata yalnız iki ayarın bir arada bulunmasında görünüyorsa, bu tam olarak ddmin'in çözdüğü durumdur. Anahtar listesini girdi olarak verip predicate'te uygulamayı o ayarlarla ayağa kaldıran bir test yazarsın. NuGet paket sürümleri de benzer bir aralıktır: bir güncellemede 12 paket birden yükseldiyse, sürümleri yarı yarıya geri alarak hangisinin davranışı değiştirdiğini bulabilirsin.
Üretimde Dikkat
Bisect'in güvenilir olması için her commit'in aynı koşullarda derlenip test edilebilmesi gerekir. Paket sürümleri sabitlenmemişse (lock file yoksa ya da floating version kullanılıyorsa), eski bir commit bugün farklı paketlerle derlenir ve test sonucu o günkü davranışı yansıtmaz. Bisect'i build ortamı belli bir makinede, ideal olarak CI (Continuous Integration) image'ının aynısında çalıştır.
Bisect script'ini production'a, paylaşılan bir test veritabanına ya da bir ödeme sağlayıcısının sandbox'ına bağlama. Her adım kodu değiştirir ve script'i yeniden çalıştırır; yan etkisi olan bir test, her adımda o yan etkiyi tekrarlar. Script'in kendi küçük, izole dünyasında çalışması gerekir: bizim probe'umuz yalnız bellekte bir hesap yapıyor.
Delta debugging'de girdiyi production'dan alıyorsan, küçültülmüş sonuç da gerçek veridir. Üç satırlık minimal dosyayı bir test fixture'ı olarak repository'ye koymadan önce içinde müşteri ya da tedarikçi verisi kalmadığından emin ol; gerekirse değerleri anonimleştir ve predicate'in hâlâ "fails" dediğini doğrula.
Sık Düşülen Tuzaklar
En sinsi tuzak, aynı belirtiyi veren birden fazla bug. Predicate'i gevşek yazarsan ddmin aradığın bug'ı değil, en kolay üretilen bug'ı bulur. Yukarıdaki predicate'i "herhangi bir exception" olacak şekilde değiştirip aynı dosyayla çalıştırdım:
catch (Exception ex) when (ex.Message.Length > 0)
input lines: 5000, fails: True
minimal lines: 2, tests run: 17
978-605-1025-00-0,Quiet Compiler 2500,691.90,12
978-605-1025-01-1,Clean Pages 2501,358.50,9
Sonuç çok daha hızlı geldi, yalnız 17 denemede, ve tamamen yanlış: iki rastgele satır, duplicate ISBN yok. ddmin header satırını atmış; header'sız dosyada ilk veri satırı header sanılıyor, Array.IndexOf -1 dönüyor ve cells[-1] bir IndexOutOfRangeException fırlatıyor. Bu da bir bug, ama bizim bug'ımız değil. Aynı tuzak bisect'te de var: "testler kırmızı" kriteriyle bisect yaparsan, aradığın hatayı değil, aralıktaki herhangi bir kırmızı testi getiren commit'i bulursun. Kriteri her zaman tek, belirli bir belirtiye bağla.
İkinci tuzak, bisect script'ini repository'nin içinde tutmak. Bisect commit değiştirdikçe script'in kendisi de değişebilir ya da eski commit'lerde hiç bulunmayabilir. Script'i ve kontrol programını repository dışında tut.
Üçüncü tuzak, derlenmeyen commit'leri "bad" saymak. dotnet build başarısız olduğunda 1 dönen bir script, bisect'i hatanın değil build hatasının peşine düşürür. Build hatası 125'tir.
Dördüncü tuzak, bulunan commit'i kanıt sanmak. Bisect sana "davranışın değiştiği ilk commit"i söyler; o commit'in neden hatalı olduğunu diff'i okuyarak ve gerekirse o commit üzerinde tek değişkenli bir deney yaparak doğrula. Bizim vakada diff, kaybolan argümanı açıkça gösteriyor.
Küçük Bir Deneme
★☆☆ Kendi projende son iki tag arasındaki commit sayısını git log --oneline v1..v2 | wc -l ile say ve bisect'in en kötü durumda kaç adım süreceğini hesapla. Sonra küçük bir deterministik testle gerçekten çalıştır.
★★☆ DeltaDebugger.Minimize'ı karakter düzeyinde bir girdiyle dene: bir JSON (JavaScript Object Notation) parser'ını patlatan uzun bir string'i, aynı exception'ı veren en kısa string'e indir. Predicate'i exception tipine ve mesajına bağlamayı unutma.
★★★ Bisect script'ini, bir hot-fix commit'ini her adımda git cherry-pick --no-commit ile uygulayıp testi koşan ve sonra git reset --hard ile geri alan bir hale getir. Git dokümanında anlatılan bu teknik, aralığın bir kısmında başka bir bug'ın testi maskelediği durumlarda işe yarar.
Elimizde artık hatanın hangi satırda, hangi commit'le ve hangi girdiyle ortaya çıktığı var. Bir sonraki yazıda bakış açısını değiştirip hatanın bize kendisini anlattığı yere, hata mesajına ve stack trace'e bakıyoruz: .NET exception'larının anatomisi, async stack'ler ve Angular'ın NG0100 gibi hata kodları.