Git bisect ve git worktree: Hatayı Bulan Commit ve Aynı Anda İki Branch
Hatayı getiren commit'i git bisect ile ikili aramayla buluyoruz: good ve bad işaretlemek, bisect log ve reset, git bisect run ile çıkış koduna göre otomatik arama ve exit 125. Ardından git worktree ile yarım işi bozmadan ayrı bir klasörde hotfix yapıyor, worktree list/remove/prune'u ve stash ile ne zaman yer değiştirdiğini görüyoruz.
Sürüm notu: Bu yazıdaki komutlar Git 2.54 ile denenmiştir.
Önce Tahmin Et
v1.0.0 sorunsuz çalışıyordu. Bugün main'de sepet toplamı bozuk çıkıyor. Testi iki noktada çalıştıralım:
$ git log --oneline v1.0.0..main
fdb91a4 (HEAD -> main) Update book titles
35606e6 Add discount function
5551b73 Document search in README
8b0b603 Add findById helper
699a57f Refactor cart total
129c4f4 Add wishlist module
$ node test/cart.test.js
FAIL: expected 95, got [object Object]35
$ git switch --detach v1.0.0
HEAD is now at b68648b Add price formatter
$ node test/cart.test.js
PASS
$ git switch main
Previous HEAD position was b68648b Add price formatter
Switched to branch 'main'
Arada altı commit var. Hatayı getiren commit'i bulmak için en kötü ihtimalle kaç commit'te test çalıştırman gerekir? Altı mı, üç mü, daha az mı? Altı yüz commit olsaydı?
Bisect'in Fikri: İkili Arama
Sözlükte bir kelime ararken ilk sayfadan başlamazsın. Ortadan açarsın, kelime daha önceyse ilk yarıya, sonraysa ikinci yarıya geçer, her adımda aralığı ikiye bölersin. git bisect aynısını geçmişte yapar. Ona iyi bilinen bir commit (good) ve bozuk bilinen bir commit (bad) verirsin. Git ortadaki commit'i checkout eder, sen test edip sonucu söylersin, aralık yarıya iner.
Altı commit için en fazla üç test yeter. Altı yüz commit için yaklaşık on. Bin commit için de on. Geçmiş büyüdükçe bisect'in değeri artar; bir bug'ın hangi commit'le geldiğini bilmek, çoğu zaman bug'ın yarısını çözmektir.
Elle Bisect: good ve bad
$ git bisect start
status: waiting for both good and bad commits
$ git bisect bad
status: waiting for good commit(s), bad commit known
$ git bisect good v1.0.0
Bisecting: 2 revisions left to test after this (roughly 2 steps)
[8b0b603771b22445b3cf4da3b0bac584dd64552c] Add findById helper
bad argümansız verilince şu anki commit'i, yani main'i bozuk sayar. good için tag'i verdik; bir önceki yazıda tag'lerin neden işe yaradığını görmüştük. Git hemen ortadaki commit'e geçti. Test et, sonucu bildir:
$ node test/cart.test.js
FAIL: expected 95, got [object Object]35
$ git bisect bad
Bisecting: 0 revisions left to test after this (roughly 1 step)
[699a57f873baee70115bca5b5548eb686f612c67] Refactor cart total
$ node test/cart.test.js
FAIL: expected 95, got [object Object]35
$ git bisect bad
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[129c4f48a163d1ede8f08e9d265c8a69349db754] Add wishlist module
$ node test/cart.test.js
PASS
$ git bisect good
699a57f873baee70115bca5b5548eb686f612c67 is the first bad commit
commit 699a57f873baee70115bca5b5548eb686f612c67
Author: Ada Lovelace <ada@example.com>
Date: Thu Nov 12 10:10:00 2026 +0300
Refactor cart total
src/cart.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
Üç testte bulduk: Refactor cart total. git show 699a57f ile bakınca sebep ortaya çıkar: refactor sırasında reduce'un başlangıç değeri olan 0 düşmüş. reduce başlangıç değeri verilmezse ilk element'i başlangıç kabul eder, toplam bir object ile bir sayının birleşimine döner.
Bisect sürerken HEAD detached durumdadır ve git status bunu söyler:
$ git status
HEAD detached at 129c4f4
You are currently bisecting, started from branch 'main'.
(use "git bisect reset" to get back to the original branch)
nothing to commit, working tree clean
$ git bisect log
git bisect start
# status: waiting for both good and bad commits
# bad: [fdb91a4064fe7096cb698ad89c27046f88692b6a] Update book titles
git bisect bad fdb91a4064fe7096cb698ad89c27046f88692b6a
...
# first bad commit: [699a57f873baee70115bca5b5548eb686f612c67] Refactor cart total
$ git bisect reset
Previous HEAD position was 129c4f4 Add wishlist module
Switched to branch 'main'
git bisect log verdiğin her kararı kaydeder. Bir adımı yanlış işaretlediysen bu çıktıyı bir dosyaya yazar, hatalı satırı düzeltir ve git bisect replay <dosya> ile aramayı oradan yeniden oynatırsın. git bisect reset ise seni bisect başlamadan önceki branch'e geri götürür; bitirdiğinde unutma.
git bisect run ile Otomatik Arama
Her adımda testi elle çalıştırıp sonucu yazmak yerine bu işi Git'e bırakabilirsin. Tek şart, test komutunun çıkış koduyla konuşması: 0 good, 1 ile 127 arası bad. 125 ise özel bir anlam taşır: "bu commit test edilemiyor, atla". 128 ve üstü bisect'i durdurur.
$ git bisect start main v1.0.0
Bisecting: 2 revisions left to test after this (roughly 2 steps)
[8b0b603771b22445b3cf4da3b0bac584dd64552c] Add findById helper
$ git bisect run node test/cart.test.js
running 'node' 'test/cart.test.js'
FAIL: expected 95, got [object Object]35
Bisecting: 0 revisions left to test after this (roughly 1 step)
[699a57f873baee70115bca5b5548eb686f612c67] Refactor cart total
running 'node' 'test/cart.test.js'
FAIL: expected 95, got [object Object]35
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[129c4f48a163d1ede8f08e9d265c8a69349db754] Add wishlist module
running 'node' 'test/cart.test.js'
PASS
699a57f873baee70115bca5b5548eb686f612c67 is the first bad commit
commit 699a57f873baee70115bca5b5548eb686f612c67
Author: Ada Lovelace <ada@example.com>
Date: Thu Nov 12 10:10:00 2026 +0300
Refactor cart total
src/cart.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
bisect found first bad commit
$ git bisect reset
Previous HEAD position was 129c4f4 Add wishlist module
Switched to branch 'main'
git bisect start main v1.0.0 bad ve good'u tek satırda verir: önce bad, sonra good. Gerisi tamamen otomatik.
Test yalnızca yeni commit'lerde varsa ya da bazı commit'ler derlenmiyorsa, test script'ini repo'nun dışına koy ve derlenmeyen durumlarda exit 125 döndür. Elle aramada da aynı iş için git bisect skip var. Bu yazıdaki test zaten v1.0.0'dan önce eklendiği için her commit'te mevcut. CI'da her commit'te testleri koşturmanın bir faydası da bu: bisect'in kullanabileceği güvenilir bir "good" her zaman elinin altında olur.
Git 2.56 git bisect'e --reset-when-found adında bir seçenek ekledi. Yerel sürümüm 2.54 olduğu için burada çalıştırmadım; sürümler bölümünde ele alacağız.
Worktree: Aynı Repo, İki Klasör
Şimdi başka bir sahne. Ada feature/discount-rules üzerinde çalışıyor ve yarım kalmış değişiklikleri var. Tam o sırada sepet hatası için acil bir hotfix isteniyor. Klasik çözümler malum: değişiklikleri stash'le ya da yarım bir "wip" commit'i at, branch değiştir, hotfix'i yap, geri dön, stash'i aç. Bir de bu sırada node_modules ya da derleme çıktıları branch'ler arasında karışabilir.
git worktree başka bir yol sunar: aynı repo'nun ikinci bir branch'ini ayrı bir klasöre checkout etmek. İki klasör tek bir .git veritabanını paylaşır; commit'ler, branch'ler, stash'ler ortaktır. Ama her klasörün kendi working tree'si, kendi index'i ve kendi HEAD'i vardır. Aynı mutfakta iki ayrı tezgâh gibi düşün: malzeme dolabı ortak, tezgâhtaki iş ayrı.
Hotfix İçin Worktree Açmak
$ git status -s
M src/discount.js
$ git worktree add -b hotfix/cart-total ../bookstore-hotfix main
Preparing worktree (new branch 'hotfix/cart-total')
HEAD is now at fdb91a4 Update book titles
$ git worktree list
.../bookstore fdb91a4 [feature/discount-rules]
.../bookstore-hotfix fdb91a4 [hotfix/cart-total]
(Yolların başını kısalttım.) -b hotfix/cart-total yeni bir branch açar, ../bookstore-hotfix klasörün yeri, main de başlangıç noktası. Yarım değişikliklerine hiç dokunmadın. Yeni klasöre geçip hotfix'i yapalım:
$ cd ../bookstore-hotfix
$ git status
On branch hotfix/cart-total
nothing to commit, working tree clean
$ node test/cart.test.js
PASS
$ git commit -qam "Fix cart total for multiple items"
$ git log --oneline -2
7bfcb12 (HEAD -> hotfix/cart-total) Fix cart total for multiple items
fdb91a4 (main, feature/discount-rules) Update book titles
reduce'a 0'ı geri ekledik, test geçti, commit attık. Burada bir kural var: aynı branch iki worktree'de aynı anda checkout edilemez.
$ git switch feature/discount-rules
fatal: 'feature/discount-rules' is already used by worktree at '.../bookstore'
Bu bir kısıtlama değil, koruma. İki klasörde aynı branch'e commit atılabilseydi biri diğerinin altından branch'i kaydırırdı. Ana klasöre dönelim:
$ cd ../bookstore
$ git status -s
M src/discount.js
$ git log --oneline -1 hotfix/cart-total
7bfcb12 (hotfix/cart-total) Fix cart total for multiple items
$ git branch
* feature/discount-rules
+ hotfix/cart-total
main
Yarım değişiklik yerinde duruyor ve hotfix commit'i bu klasörden de görünüyor, çünkü repo tek. git branch çıktısındaki +, o branch'in başka bir worktree'de checkout edildiğini gösterir. Hotfix'i buradan push'layabilir, PR açabilirsin.
Worktree'yi Kapatmak
İş bitince worktree'yi Git'e kaldırtırsın:
$ git worktree remove ../bookstore-hotfix
$ git worktree list
.../bookstore fdb91a4 [feature/discount-rules]
Klasör silindi, branch ve commit'ler duruyor. Worktree'de commit'lenmemiş bir şey varsa Git seni durdurur:
$ git worktree add --detach ../bookstore-review2 main
Preparing worktree (detached HEAD fdb91a4)
HEAD is now at fdb91a4 Update book titles
$ git worktree remove ../bookstore-review2
fatal: '../bookstore-review2' contains modified or untracked files, use --force to delete it
--detach branch'siz bir worktree açar; bir PR'ı ya da eski bir sürümü sadece incelemek için birebirdir. Klasörü Finder'dan ya da rm ile sildiysen Git kaydı bir süre tutmaya devam eder; git worktree prune artık var olmayan worktree kayıtlarını temizler.
Stash mı, Worktree mi?
Stash yazısında stash'i "kısa bir ara" için önermiştik ve o hâlâ doğru. Beş dakikalık bir kesinti, tek dosyalık bir değişiklik, aynı klasörde hemen geri döneceğin bir iş: stash yeter.
Worktree şu durumlarda kazanır. Kesinti uzun sürecekse ve stash'e attığını unutma ihtimalin varsa. İki branch'i yan yana çalıştırıp karşılaştırman gerekiyorsa, örneğin eski ve yeni sürümün çıktısına aynı anda bakmak. Uzun süren bir test ya da build bir branch'te koşarken diğerinde çalışmaya devam etmek istiyorsan. Bir de PR review'ları: reviewer olarak bir PR'ı kendi işini bozmadan ayrı bir klasörde açmak.
Bedeli de var: her worktree tam bir checkout'tur, bağımlılıklarını ayrıca kurman gerekir (node_modules her klasörde ayrı). Worktree'leri repo klasörünün içine değil, yanına aç; yoksa ana repo onları untracked dosya olarak görür.
Sık Düşülen Tuzaklar
Bisect'te bir adımı yanlış işaretlemek. Tek bir yanlış good/bad, aramayı yanlış commit'e götürür ve Git bunu fark edemez. Emin değilsen git bisect skip de, ya da git bisect log ile kararları gözden geçir.
git bisect reset'i unutmak. Detached HEAD'de kalırsın ve bir sonraki commit'in hiçbir branch'e ait olmaz.
bisect run script'inin çıkış kodunu kontrol etmemek. Hata durumunda 0 dönen bir script her commit'i good sayar. Script'i önce bilinen bad ve good commit'te elle çalıştır.
Worktree'yi rm -rf ile silmek. Klasör gider ama Git'in kaydı kalır ve branch "başka worktree'de kullanılıyor" görünmeye devam eder. git worktree remove kullan, unuttuysan git worktree prune.
Aynı branch'i iki worktree'de açmaya çalışmak. Git izin vermez. İkinci klasörde yeni bir branch aç ya da --detach kullan.
Küçük Bir Deneme
★☆☆ bookstore repo'nda on commit'lik bir geçmiş oluştur ve bilerek yedinci commit'te bir testi boz. Elle git bisect ile kaç adımda bulduğunu say, sonucu log₂(10) ile karşılaştır.
★★☆ Aynı aramayı git bisect run ile yap. Sonra test script'ini repo dışına taşı, bir commit'te bilerek derlenmeyen bir dosya bırak ve script'in o durumda exit 125 döndürmesini sağla. git bisect log çıktısında atlanan commit'i bul.
★★★ Yarım değişiklikler varken git worktree add ile bir hotfix branch'i aç, hotfix'i commit'le, ana klasörde main'e merge et ve worktree'yi kaldır. Ardından aynı senaryoyu stash ile yap ve iki yolda kaç komut çalıştırdığını karşılaştır.
Bisect ve worktree ile günlük Git kas hafızası tamamlanıyor. Bir sonraki yazıda Git'in otomatik devreye giren tarafına geçeceğiz: commit atmadan önce çalışan hook'lar, Git 2.54 ile gelen config tabanlı hook'lar ve commit'leri SSH anahtarıyla imzalayıp GitHub'da "Verified" rozetini almak.