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

Git reflog: Kaybolan Commit'leri ve Silinen Branch'leri Geri Getirmek

Mert Özen 6 Kas 2026 8 dk 1 görüntülenme
Git reflog: Kaybolan Commit'leri ve Silinen Branch'leri Geri Getirmek

Yanlışlıkla yapılan bir reset --hard'dan sonra commit'leri git reflog ile geri getiriyoruz: HEAD@{n} ile HEAD~n farkı, ORIG_HEAD ile son merge'ü geri almak, silinen branch'i reflog'dan bulmak, branch'in kendi reflog'u ile rebase'den dönmek, reflog'un yerel ve süreli olması.

Sürüm notu: Bu yazıdaki komutlar Git 2.54 ile denenmiştir.

Önce Tahmin Et

Ada son iki commit'ini tek commit'te toplamak istedi. Aklındaki komut git reset --soft HEAD~2 idi, ama parmakları alışkanlıkla başka bir şey yazdı:

$ git log --oneline -3
75253fa (HEAD -> main) Add total label to cart
a2af014 Add price formatter
077420f Add .gitignore
$ git reset --hard HEAD~2
HEAD is now at 077420f Add .gitignore
$ git log --oneline -3
077420f (HEAD -> main) Add .gitignore
24aba37 Add cart total
f7f661c Add book list

İki commit git log'dan kayboldu, working tree'deki dosyalar da gitti. Bu commit'ler hiçbir yere push'lanmamıştı. Sence şu an hâlâ repo'nun içinde bir yerdeler mi, yoksa geri dönüşü olmayan bir şekilde silindiler mi?

Reflog: HEAD'in Seyir Defteri

Cevap: oradalar. git log sana commit'lerin birbirine bağlı geçmişini gösterir, yani bir branch'ten geriye doğru parent zincirini. Ama Git ayrıca HEAD'in ve her branch'in ucunun nereye gittiğini, ne zaman ve hangi komutla gittiğini ayrı bir deftere yazar. Bu defterin adı reflog.

$ git reflog
077420f (HEAD -> main) HEAD@{0}: reset: moving to HEAD~2
75253fa HEAD@{1}: commit: Add total label to cart
a2af014 HEAD@{2}: commit: Add price formatter
077420f (HEAD -> main) HEAD@{3}: checkout: moving from feature/wishlist to main
e1f0c14 (feature/wishlist) HEAD@{4}: commit: Document wishlist in README
76745bd HEAD@{5}: commit: Add wishlist module
077420f (HEAD -> main) HEAD@{6}: checkout: moving from main to feature/wishlist
077420f (HEAD -> main) HEAD@{7}: commit: Add .gitignore
24aba37 HEAD@{8}: commit: Add cart total
f7f661c HEAD@{9}: commit: Add book list
0d19b57 HEAD@{10}: commit (initial): Add README

En yeni kayıt en üstte. HEAD@{0} şu an, HEAD@{1} "bir önceki hamlede HEAD'in olduğu yer", HEAD@{2} ondan da önceki. Her satırda hamleyi yapan komut yazar: commit, checkout, reset, merge, rebase. Bir uçağın kara kutusu gibi düşün; uçağın nereden geçtiğini harita değil kara kutu söyler.

Burada önemli bir ayrım var. HEAD@{1} "HEAD'in bir önceki commit'i" değildir; o HEAD~1'dir ve parent zincirinde bir adım geri gider. HEAD@{1} ise zamanda bir adım geri gider: HEAD'in bir önceki hamlede gösterdiği commit. Reset'ten hemen sonra bu ikisi çok farklı yerlerdir.

Kaybolan Commit'i Geri Getirmek

Reset'ten önce HEAD 75253fa'daydı ve reflog bunu HEAD@{1} olarak gösteriyor. Geri dönmek için aynı reset'i, bu kez doğru hedefe yaparsın:

$ git reset --hard HEAD@{1}
HEAD is now at 75253fa Add total label to cart
$ git log --oneline -3
75253fa (HEAD -> main) Add total label to cart
a2af014 Add price formatter
077420f Add .gitignore

İki commit ve dosyalar geri geldi. Asıl yapmak istediğin --soft reset'i şimdi rahatça yapabilirsin.

Branch'i oynatmadan önce ne bulduğuna bakmak istersen, kayıp commit'e önce bir isim vermek daha temkinli bir yoldur: git branch rescue 75253fa. Commit'e bir branch işaret ettiği an artık "kayıp" değildir. İçine git log rescue ile bakar, emin olunca istediğin yere taşırsın.

ORIG_HEAD: Son Büyük Hamleyi Geri Almak

Git, HEAD'i büyük ölçüde oynatan komutlardan önce eski yeri ORIG_HEAD adında özel bir ref'e yazar. reset, merge ve rebase bunu yapar. Ada feature/wishlist'i main'e merge etti, sonra bunun erken olduğuna karar verdi:

$ git merge --no-ff feature/wishlist
Merge made by the 'ort' strategy.
 README.md       | 4 ++++
 src/wishlist.js | 7 +++++++
 2 files changed, 11 insertions(+)
 create mode 100644 src/wishlist.js
$ git reset --hard ORIG_HEAD
HEAD is now at 75253fa Add total label to cart
$ git log --oneline -2
75253fa (HEAD -> main) Add total label to cart
a2af014 Add price formatter

ORIG_HEAD tek bir değer tutar ve her büyük hamlede üzerine yazılır. Yani yalnızca "son hamle" için güvenilirdir. Arada başka bir reset ya da rebase yaptıysan reflog'a dön. Bir de merge'ü push'ladıysan bu yol kapanır; paylaşılmış bir merge'ü geri almanın yolu revert yazısında gördüğümüz git revert -m 1'dir.

Silinen Branch'i Geri Getirmek

Ada feature/wishlist'in işinin bittiğini düşündü ve büyük harfle sildi:

$ git branch -D feature/wishlist
Deleted branch feature/wishlist (was e1f0c14).

Hemen fark ettiysen en kolay yol o çıktının kendisi: (was e1f0c14). O hash'le branch'i yeniden oluşturursun. Ama terminali kapattıysan ya da çıktıyı görmediysen reflog'a dönersin. Önce son kayıtlara bakalım:

$ git reflog -6
75253fa (HEAD -> main) HEAD@{0}: reset: moving to ORIG_HEAD
d1e7936 HEAD@{1}: merge feature/wishlist: Merge made by the 'ort' strategy.
75253fa (HEAD -> main) HEAD@{2}: reset: moving to HEAD@{1}
077420f HEAD@{3}: reset: moving to HEAD~2
75253fa (HEAD -> main) HEAD@{4}: commit: Add total label to cart
a2af014 HEAD@{5}: commit: Add price formatter

Branch silmek HEAD'i oynatmadığı için HEAD'in reflog'unda bir satır bırakmaz; branch'in kendi reflog'u da branch'le birlikte silinir. Aradığın şey, o branch'teyken attığın son commit ya da o branch'ten ayrıldığın an. Liste uzunsa süzersin:

$ git reflog | grep wishlist
d1e7936 HEAD@{1}: merge feature/wishlist: Merge made by the 'ort' strategy.
077420f HEAD@{6}: checkout: moving from feature/wishlist to main
e1f0c14 HEAD@{7}: commit: Document wishlist in README
76745bd HEAD@{8}: commit: Add wishlist module
077420f HEAD@{9}: checkout: moving from main to feature/wishlist

checkout: moving from feature/wishlist to main satırı, branch'ten ayrıldığın an. Hemen altındaki kayıt da o branch'te attığın son commit: e1f0c14.

$ git branch feature/wishlist e1f0c14
$ git log --oneline -2 feature/wishlist
e1f0c14 (feature/wishlist) Document wishlist in README
76745bd Add wishlist module

Şuna dikkat et: yazının başındaki reflog'da e1f0c14 HEAD@{4}'tü, şimdi HEAD@{7}. Her yeni hamle numaraları bir kaydırır. Reflog'da bir numara bulduktan sonra arada başka komut çalıştırdıysan listeye yeniden bak; en güvenlisi numara yerine hash'i kullanmak.

Branch'in Kendi Reflog'u: Rebase'i Geri Almak

HEAD'in reflog'u her şeyi tek listede tutar ve rebase gibi çok adımlı işlemlerde kalabalıklaşır. Her branch'in ayrıca kendi reflog'u vardır ve o yalnızca o branch'in ucunun hareketlerini kaydeder. Ada feature/wishlist'i main'e rebase'ledi:

$ git switch feature/wishlist
Switched to branch 'feature/wishlist'
$ git rebase main
Successfully rebased and updated refs/heads/feature/wishlist.
$ git reflog show feature/wishlist
7ffad63 (HEAD -> feature/wishlist) feature/wishlist@{0}: rebase (finish): refs/heads/feature/wishlist onto 75253fa6130576b4458ac263bd8be0a9f6c0662a
e1f0c14 feature/wishlist@{1}: branch: Created from e1f0c14

İki satır: biri rebase'in bitişi, diğeri branch'in yeniden oluşturulduğu an. Rebase öncesi hâl feature/wishlist@{1}. Rebase'den vazgeçmek için:

$ git reset --hard feature/wishlist@{1}
HEAD is now at e1f0c14 Document wishlist in README
$ git log --oneline -3
e1f0c14 (HEAD -> feature/wishlist) Document wishlist in README
76745bd Add wishlist module
077420f Add .gitignore

Interactive rebase'de yanlışlıkla bir satır sildiğinde de çözüm aynı: branch'in reflog'unda rebase'den önceki kaydı bul ve oraya dön. Aynı @{} yazımı zamanla da çalışır: main@{yesterday} ya da HEAD@{2.hours.ago} gibi ifadeler, o ref'in o zamanki hâlini verir.

Reflog Yereldir ve Süresi Vardır

Reflog senin makinende, .git/logs altında durur. Push'lanmaz, clone'lanmaz. Aynı repo'yu yeniden clone'larsan defter sıfırdan başlar:

$ cd ..
$ git clone -q bookstore bookstore-copy
$ cd bookstore-copy
$ git reflog
75253fa (HEAD -> main, origin/main, origin/HEAD) HEAD@{0}: clone: from .../bookstore

(Yolun başını kısalttım.) Tek bir satır. Bunun pratik anlamı şu: ekip arkadaşının kaybettiği commit'i senin reflog'unda bulamazsın, kendi makinesinde araması gerekir. Bir CI makinesi de her işte temiz bir clone'la başladığı için orada da reflog'a güvenilmez. GitHub'daki repo'nun da senin göreceğin bir reflog'u yoktur.

Reflog kayıtları sonsuza kadar da durmaz. git gc çalıştığında gc.reflogExpire süresinden eski kayıtlar silinir; varsayılan 90 gün. O ref'in şimdiki ucundan ulaşılamayan commit'lere ait kayıtlar için ayrı bir süre var, gc.reflogExpireUnreachable, varsayılanı 30 gün. Kayıt silindikten sonra hiçbir şeyin göstermediği commit'ler de bir sonraki temizlikte gerçekten silinir. Yani reflog bir geri dönüşüm kutusudur, arşiv değil. Bir şeyi kaybettiysen haftalar sonra değil, hemen ara.

Sık Düşülen Tuzaklar

Hiç commit'lenmemiş değişikliği reflog'da aramak. Reflog commit'leri kaydeder. reset --hard ya da restore ile sildiğin, hiç commit'lenmemiş satırlar orada yoktur. git add ile index'e alınmış dosyaların içeriği bir süre object olarak durur ve git fsck --lost-found ile bulunabilir, ama bu son çaredir. En iyi koruma sık commit atmaktır.

HEAD@{1} ile HEAD~1'i karıştırmak. Biri zamanda, diğeri parent zincirinde geri gider. Reset'ten sonra yanlışını kullanırsan bir commit daha kaybedersin.

Eski numaraya güvenmek. Reflog numaraları her hamlede kayar. Okuduğun numarayı kullanmadan önce arada komut çalıştırdıysan listeye yeniden bak.

Reflog'u yedek sanmak. Yerel, süreli ve makineye bağlı. Gerçek yedek push'lanmış bir branch'tir.

git gc --prune=now ve git reflog expire --expire=now gibi komutları "repo'yu temizlemek" için ezbere çalıştırmak. Bunlar tam olarak bu yazıdaki kurtarma yollarını kapatır.

Küçük Bir Deneme

★☆☆ bookstore repo'nda üç commit at, git reset --hard HEAD~3 yap, sonra commit'leri hem HEAD@{n} ile hem de önce git branch rescue <hash> ile geri getir. İki yolu karşılaştır.

★★☆ Bir branch oluştur, iki commit at, main'e dön ve branch'i -D ile sil. Silme çıktısına bakmadan, yalnızca git reflog ile branch'i geri getir. Hangi satırı aradığını tek cümleyle yaz.

★★★ Bir branch'i interactive rebase ile toparla ve bir commit'i bilerek drop et. Sonra git reflog show <branch> ile rebase öncesine dön, git diff <branch>@{1} <branch> gibi bir karşılaştırmayla hangi değişikliğin geri geldiğini doğrula.

Reflog sana "HEAD ne zaman nerede durdu" bilgisini veriyor; ama bir commit'e kalıcı ve herkesin göreceği bir isim vermenin yolu başka. Bir sonraki yazıda tag'lerle sürümleri işaretleyecek, lightweight ve annotated tag farkını, git describe'ı ve GitHub Releases'ı göreceğiz.