Bir commit'i geri almanın tek bir doğru komutu yok. Doğru komut iki sorunun cevabına bağlı: Commit makinenizden çıktı mı, yani push edildi mi ya da başka biri onu çekti mi? Commit'teki değişiklikleri saklamak mı istiyorsunuz, yoksa tamamen atmak mı? Bu rehberde git reset, git revert, git restore ve git commit --amend arasından bu iki soruya göre seçim yapmayı, ters giden bir geri almanın ardından kaybolan commit'leri reflog ile kurtarmayı ve commit GitHub ya da GitLab'a ulaştıktan sonra nelerin değiştiğini örneklerle anlatıyoruz. Aşağıdaki çıktıların tamamı, Ekim 2026'da Git 2.54 ile magaza-api adlı örnek bir depoda komutlar tek tek çalıştırılarak alındı; sizin deponuzdaki commit kimlikleri (hash) farklı olacaktır.
30 saniyede karar tablosu
| Durum | Komut | Commit push edildiyse |
|---|---|---|
| Son commit'in mesajını düzeltmek ya da unutulan bir dosyayı eklemek | git commit --amend | Kaçının: geçmişi yeniden yazar |
| Son yerel commit'i geri almak, değişiklikler hazırlanmış (staged) kalsın | git reset --soft HEAD~1 | Kaçının |
| Son yerel commit'i geri almak, değişiklikler dosyalarda kalsın | git reset HEAD~1 (varsayılan mod --mixed) | Kaçının |
| Commit'i ve içindeki değişiklikleri tamamen silmek | git reset --hard HEAD~1 (tehlikeli; daha güvenlisi --keep) | Kaçının |
| Push edilmiş, paylaşılan bir commit'i geri almak | git revert HASH | Doğru araç budur |
| Push edilmiş bir birleştirmeyi (merge) geri almak | git revert -m 1 MERGE_HASH | Doğru araç budur |
| Bir dosyayı hazırlama alanından çıkarmak (unstage) | git restore --staged DOSYA | Commit'e dokunmaz |
| Dosyadaki commit'lenmemiş değişikliği atmak | git restore DOSYA (geri dönüşü yok) | Commit'e dokunmaz |
| Dosyanın eski bir commit'teki hâlini geri getirmek | git restore --source=HASH DOSYA | Commit'e dokunmaz |
| Hatalı reset, rebase ya da amend sonrası kaybolan commit'i bulmak | git reflog, ardından git branch kurtarma HASH | Yalnızca yerel depoda işe yarar |
| Çakışmayla duran birleştirmeden vazgeçmek | git merge --abort | Henüz commit yok |
Commit push edildi mi?
Tablodaki en önemli sütun budur ve cevabı tahmin etmeniz gerekmez:
Kod:
$ git status
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
(use "git push" to publish your local commits)
nothing to commit, working tree clean
$ git log --oneline origin/main..HEAD
cb72544 (HEAD -> main) Sayfalama hatsı düzeltildi
"ahead of 'origin/main' by 1 commit" ifadesi bir commit'in henüz gönderilmediğini söyler; origin/main..HEAD aralığı da gönderilmemiş commit'leri tek tek listeler. git branch -r --contains HASH, commit'i içeren uzak dalları yazar: örneğimizde push'tan önce boş çıktı verdi, push'tan sonra origin/main yazdı. Bu komutlar son git fetch anındaki bilgiyle çalışır; emin olmak için önce git fetch çalıştırın.
Zihinsel model: üç alan, bir işaretçi
Git'te bir dosyanın aynı anda üç hâli bulunabilir: diskte düzenlediğiniz çalışma ağacı (working tree), git add ile bir sonraki commit için hazırladığınız hazırlama alanı (staging area ya da index) ve commit'lenmiş geçmiş. HEAD o an bulunduğunuz yeri gösterir; normalde bir dala, dal da bir commit'e işaret eder. Geri alma komutlarının her biri bu alanlardan birini ya da dal işaretçisini değiştirir:
Kod:
Çalışma ağacı Hazırlama alanı Commit geçmişi
(diskteki dosyalar) (index, staging area)
app.py --- git add -------→ app.py --- git commit --→ 556f46e ← main ← HEAD
←-- git restore ---- ←-- git restore --staged --
Reset ile revert arasındaki fark en iyi geçmiş üzerinde görülür:
Kod:
git reset HEAD~1 → dal ucu geri çekilir, geçmiş yeniden yazılır
önce : A---B---C---D main
sonra: A---B---C main (D hiçbir daldan görünmez, reflog'da durur)
git revert D → D'nin tersi yeni bir commit olarak eklenir, geçmiş korunur
önce : A---B---C---D main
sonra: A---B---C---D---D' main (D' = D'nin değişikliklerinin tersi)
| Komut | Dal ucu (HEAD) | Hazırlama alanı | Çalışma ağacı |
|---|---|---|---|
| git reset --soft X | X'e taşınır | Değişmez | Değişmez |
| git reset --mixed X (varsayılan) | X'e taşınır | X'e eşitlenir | Değişmez |
| git reset --hard X | X'e taşınır | X'e eşitlenir | X'e eşitlenir; izlenen dosyalardaki commit'lenmemiş değişiklikler silinir |
| git reset --keep X | X'e taşınır | X'e eşitlenir | Yalnızca HEAD ile X arasında farklı olan dosyalar güncellenir; bu dosyalarda yerel değişiklik varsa işlem iptal edilir |
| git revert X | Yeni bir commit eklenir | Güncellenir | Güncellenir |
| git restore DOSYA | Değişmez | Değişmez | Dosya hazırlama alanındaki hâline döner |
| git restore --staged DOSYA | Değişmez | Dosya HEAD'deki hâline döner | Değişmez |
git reset ayrıntılı: --soft, --mixed, --hard ve --keep
Örnek deponun başlangıç durumu aşağıdaki gibi; son commit'in eklediği hata ayıklama günlüğünü geri almak istiyoruz:
Kod:
$ git log --oneline
556f46e (HEAD -> main) Hata ayıklama günlüğü eklendi
eb201e5 Fiyat filtresi eklendi
560853b Ürün listesi uç noktası eklendi
7fc6d93 İlk commit
HEAD~1, HEAD^ ve hash
- HEAD~1 ile HEAD^ aynı commit'i gösterir: HEAD'in ilk ebeveyni (parent). HEAD~3, ilk ebeveynler üzerinden üç kuşak geridir.
- HEAD^2 başka bir şeydir: bir birleştirme (merge) commit'inin ikinci ebeveyni, yani birleştirilen dalın ucu.
- En açık yol hash yazmaktır: git reset --soft eb201e5. Kısaltılmış hash, depoda tek bir nesneye karşılık geldiği sürece yeterlidir.
- Windows'un cmd.exe kabuğunda ^ kaçış karakteridir; zsh'de extendedglob seçeneği açıksa HEAD^ yazınca "no matches found: HEAD^" hatası alırsınız. HEAD~1 yazmak iki sorunu da ortadan kaldırır.
--soft: commit gider, değişiklikler hazırlanmış kalır
Kod:
$ git reset --soft HEAD~1
$ git log --oneline
eb201e5 (HEAD -> main) Fiyat filtresi eklendi
560853b Ürün listesi uç noktası eklendi
7fc6d93 İlk commit
$ git status --short
M app.py
--mixed (varsayılan): commit gider, değişiklikler dosyalarda kalır
Kod:
$ git reset HEAD~1
Unstaged changes after reset:
M app.py
$ git status --short
M app.py
--hard: commit de değişiklikler de gider
Kod:
$ git reset --hard HEAD~1
HEAD is now at eb201e5 Fiyat filtresi eklendi
$ git status --short
--keep: --hard'ın temkinli kardeşi
--keep, commit'leri --hard gibi daldan çıkarır ama yerel değişikliklerinizi ezmez. Çıkarılan commit'lerin değiştirdiği bir dosyada sizin de yerel değişikliğiniz varsa hiçbir şeye dokunmadan durur:
Kod:
$ git reset --keep HEAD~1 # 1. deneme: yerel değişiklik README.md'de
$ git status --short
M README.md
$ git reset --keep HEAD~1 # 2. deneme, aynı başlangıçtan: yerel değişiklik app.py'de ve çıkarılacak commit de app.py'yi değiştirmişti
error: Entry 'app.py' not uptodate. Cannot merge.
fatal: Could not reset index file to revision 'HEAD~1'.
Deponun ilk commit'ini geri almak
İlk (kök) commit'in ebeveyni olmadığı için HEAD~1 çözümlenemez:
Kod:
$ git reset --soft HEAD~1
fatal: ambiguous argument 'HEAD~1': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'
$ git update-ref -d HEAD
$ git status --short
A README.md
git commit --amend: son commit'i düzeltmek
Mesajdaki bir yazım hatası ya da unutulan bir dosya için reset'e gerek yoktur:
Kod:
$ git commit -m "Sayfalama hatsı düzeltildi"
[main 4a991d0] Sayfalama hatsı düzeltildi
1 file changed, 1 insertion(+)
create mode 100644 pagination.py
$ git commit --amend -m "Sayfalama hatası düzeltildi"
[main c886d38] Sayfalama hatası düzeltildi
Date: Tue Oct 20 10:05:00 2026 +0300
1 file changed, 1 insertion(+)
create mode 100644 pagination.py
Hash'in değiştiğine dikkat edin: 4a991d0 gitti, yerine c886d38 geldi. Amend eski commit'i düzenlemez, onun yerine yenisini koyar. Unutulan bir dosya için git add DOSYA ve ardından git commit --amend --no-edit yeterlidir; --no-edit mesajı olduğu gibi bırakır. Daha eski bir commit'in mesajını değiştirmek için git rebase -i HEAD~3 çalıştırın ve ilgili satırda pick yerine reword yazın. İki yöntem de geçmişi yeniden yazar; commit push edildiyse önce aşağıdaki "Commit push edildiyse" bölümünü okuyun.
git revert: paylaşılan dalda güvenli geri alma
git revert, hedef commit'in değişikliklerini tersine çeviren yeni bir commit oluşturur. Geçmiş silinmez, kimsenin klonu bozulmaz; bu yüzden main gibi paylaşılan dallarda doğru araç budur. Komut temiz bir çalışma ağacı ister.
Kod:
$ git revert --no-edit HEAD~1
[main d788f87] Revert "Fiyat filtresi eklendi"
Date: Tue Oct 20 10:10:00 2026 +0300
1 file changed, 2 deletions(-)
delete mode 100644 filters.py
$ git log --oneline -4
d788f87 (HEAD -> main) Revert "Fiyat filtresi eklendi"
556f46e Hata ayıklama günlüğü eklendi
eb201e5 Fiyat filtresi eklendi
560853b Ürün listesi uç noktası eklendi
$ git show -s --format=%B HEAD
Revert "Fiyat filtresi eklendi"
This reverts commit eb201e58f05ada6884de959157b57ccc7e85a9a9.
--no-edit seçeneğini burada yalnızca çıktıyı kısa tutmak için kullandık. Gerçek bir projede düzenleyicinin açılmasına izin verin ve mesaja geri almanın nedenini yazın; Git belgeleri de bunu açıkça önerir. Birden fazla commit için:
Bash:
# Son iki commit için ayrı ayrı revert commit'leri (en yeniden eskiye doğru)
git revert --no-edit HEAD~2..HEAD
# Aynı geri alma tek bir commit olarak
git revert --no-commit HEAD~2..HEAD
git commit -m "Fiyat filtresi ve hata ayıklama günlüğü geri alındı"
A..B aralığı A'yı içermez: HEAD~2..HEAD, HEAD~1 ile HEAD'i geri alır. Revert sırasında çakışma (conflict) çıkarsa dosyaları düzeltin, git add ile işaretleyin ve git revert --continue çalıştırın; vazgeçmek için git revert --abort yeterlidir. GitHub'da birleştirilmiş bir pull request'in altındaki Revert düğmesi, birleştirme commit'ini geri alan yeni bir pull request açar; GitLab'da birleştirilmiş merge request'teki Revert düğmesi de aynı işi görür.
Birleştirme commit'ini geri almak: -m 1
Birleştirme commit'inin iki ebeveyni vardır ve Git hangisine göre geri alacağını kendisi seçemez:
Kod:
$ git log --oneline --graph -6
* cf3e095 (HEAD -> main) Merge branch 'feature/indirim'
|\
| * bd88e61 (feature/indirim) Ödeme sayfasında indirim gösterildi
| * 95921f9 İndirim kuralları eklendi
|/
* 556f46e Hata ayıklama günlüğü eklendi
* eb201e5 Fiyat filtresi eklendi
* 560853b Ürün listesi uç noktası eklendi
$ git revert --no-edit HEAD
error: commit cf3e0953f80aebee5b40800fbc33b74c0fadb7d0 is a merge but no -m option was given.
fatal: revert failed
$ git log -1 --format='%h %p' HEAD
cf3e095 556f46e bd88e61
$ git revert -m 1 --no-edit HEAD
[main 32f4190] Revert "Merge branch 'feature/indirim'"
Date: Tue Oct 20 10:17:00 2026 +0300
2 files changed, 4 deletions(-)
delete mode 100644 checkout.py
delete mode 100644 discount.py
%p ebeveynleri sırasıyla yazar: 1 numara birleştirmenin yapıldığı dal (burada main, 556f46e), 2 numara birleştirilen dalın ucu (bd88e61). -m 1, "ana hat birinci ebeveyndir; dal tarafından gelen değişiklikleri geri al" demektir. Pull request ya da merge request ile birleştirilen dallarda doğru değer neredeyse her zaman 1'dir.
"Revert'ü geri al" tuzağı
Birleştirmeyi revert etmek dosyalardaki değişiklikleri geri alır, geçmişi değil. Git, dalın commit'lerini hâlâ birleştirilmiş sayar. Ekip dalı düzeltip yeniden birleştirdiğinde yalnızca yeni commit'ler gelir:
Kod:
# feature/indirim dalına bir commit daha eklendi (tests/test_discount.py)
$ git merge --no-edit feature/indirim
Merge made by the 'ort' strategy.
tests/test_discount.py | 1 +
1 file changed, 1 insertion(+)
create mode 100644 tests/test_discount.py
$ ls
README.md
app.py
filters.py
tests
Birleştirme başarılı görünüyor, ama discount.py ve checkout.py yok. Doğru sıra, yeniden birleştirmeden önce revert commit'ini geri almaktır. Git 2.43 ve sonrası bu tür bir commit'e otomatik olarak "Reapply" başlığı verir:
Kod:
$ git revert --no-edit 32f4190
[main bc04174] Reapply "Merge branch 'feature/indirim'"
Date: Tue Oct 20 10:20:00 2026 +0300
2 files changed, 4 insertions(+)
create mode 100644 checkout.py
create mode 100644 discount.py
$ git merge --no-edit feature/indirim
İkinci birleştirmeyi çoktan yaptıysanız da revert'ün revert'ü eksik dosyaları geri getirir; testimizde aynı sonucu verdi. Önemli bir istisna var: ekip hatalı commit'lerden vazgeçip dalı güncel main üzerinde baştan yazdıysa (rebase), revert'ü geri almayın, yeniden yazılan dalı doğrudan birleştirin. Revert'ü geri almak eski, hatalı değişiklikleri yeni sürümlerinin yanına geri getirir ve çoğu zaman çakışmayla sonuçlanır. Git'in "revert-a-faulty-merge" belgesi bu iki senaryoyu ayrı ayrı ele alır.
Commit push edildiyse
Kural basit: Başkalarının çektiği bir dalın geçmişini ekipte konuşmadan yeniden yazmayın, revert kullanın. Geçmişi yeniden yazmayı haklı çıkaran durumlar azdır: depoya giren bir parola ya da anahtar, yanlış dala gönderilmiş commit'ler, depoyu şişiren büyük bir dosya ya da yalnızca sizin çalıştığınız bir özellik dalı.Push edilmiş bir commit'i amend ettikten sonra yerel dalınız uzak daldan ayrışır (diverged) ve normal push reddedilir. Commit'i reset ile geri aldıysanız git status dalın geride (behind) olduğunu söyler; push yine aynı "non-fast-forward" hatasıyla reddedilir:
Kod:
$ git status
On branch main
Your branch and 'origin/main' have diverged,
and have 1 and 1 different commits each, respectively.
(use "git pull" if you want to integrate the remote branch with yours)
nothing to commit, working tree clean
$ git push
To github.com:ornek/magaza-api.git
! [rejected] main -> main (non-fast-forward)
error: failed to push some refs to 'github.com:ornek/magaza-api.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Çıktıdaki git pull önerisi burada yanıltıcıdır, çünkü amacınız uzak daldaki commit'i almak değil, onun yerine yenisini koymaktır. Testimizde pull yöntemi ayarlanmamış depoda git pull, "fatal: Need to specify how to reconcile divergent branches." hatasıyla durdu; ayarlı bir depoda ise aynı değişiklik geçmişe iki kez girebilir. Doğru seçim dala bağlıdır:
- Dal paylaşılıyorsa: amend'den vazgeçin. Amend'den hemen sonra git reset --soft HEAD@{1}, dalı push ettiğiniz commit'e geri götürür (emin olmak için önce git reflog -2 çıktısına bakın). Amend ile eklediğiniz değişiklikler hazırlanmış kalır; onları yeni bir commit olarak normal şekilde gönderin. Yalnızca mesajı değiştirdiyseniz geriye değişiklik kalmaz ve yazım hatası geçmişte durur; çoğu ekip için bu kabul edilebilir bir bedeldir.
- Dal yalnızca sizinse (örneğin açık bir pull request'in dalı): git push --force-with-lease --force-if-includes.
--force-with-lease ve --force-if-includes
--force, uzak dal neye işaret ediyorsa onu ezer; siz son kez fetch ettikten sonra bir ekip arkadaşınızın gönderdiği commit'ler de buna dahildir. --force-with-lease ise uzak dalın hâlâ sizin bildiğiniz commit'te, yani origin/main uzak izleme dalınızın gösterdiği yerde olmasını şart koşar:
Kod:
$ git push --force-with-lease
To github.com:ornek/magaza-api.git
! [rejected] main -> main (stale info)
error: failed to push some refs to 'github.com:ornek/magaza-api.git'
"stale info", siz fetch ettikten sonra birinin push yaptığı anlamına gelir. Zayıf nokta şudur: IDE'niz ya da zamanlanmış bir görev arka planda git fetch çalıştırırsa origin/main güncellenir ve --force-with-lease'in koşulu, ekip arkadaşınızın commit'ini hiç görmemiş olsanız bile sağlanmış olur:
Kod:
$ git fetch -q # arka planda çalışan bir getirme gibi
$ git push --force-with-lease --dry-run
To github.com:ornek/magaza-api.git
+ 130df5a...d7e908a main -> main (forced update)
$ git push --force-with-lease --force-if-includes
To github.com:ornek/magaza-api.git
! [rejected] main -> main (remote ref updated since checkout)
error: failed to push some refs to 'github.com:ornek/magaza-api.git'
hint: Updates were rejected because the tip of the remote-tracking branch has
hint: been updated since the last checkout. If you want to integrate the
hint: remote changes, use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
--dry-run satırı, push'un geçeceğini ve 130df5a'daki ekip commit'inin silineceğini gösteriyor. Git 2.30 ile gelen --force-if-includes, uzak izleme dalının ucunun yerel dalınıza gerçekten dahil edilip edilmediğini dalın reflog'undan kontrol eder ve dahil edilmemişse push'u durdurur. Bunu kalıcı yapmak için git config --global push.useForceIfIncludes true yeterlidir; bu ayar yalnızca --force-with-lease ile birlikte çalışır, düz --force'u korumaz. Zorla göndermeden önce ekibe haber verin. Geçmişi değişen dalı kullanan ekip arkadaşlarınız dallarını merge ile değil rebase ile güncellemelidir (git pull --rebase); aksi hâlde eski commit'ler geri gelir.
Korumalı dallar (protected branches) bu işin ikinci emniyetidir. GitHub belgelerine göre her dal koruma kuralı, eşleşen dallara zorla göndermeyi (force push) varsayılan olarak engeller. GitLab'da deponun varsayılan dalı baştan korumalıdır ve "Allowed to force push" seçeneği açılmadıkça zorla gönderme reddedilir. main'e zorla gönderme reddediliyorsa bu bir arıza değil, korumanın işini yaptığının işaretidir; revert kullanın.
Depoya parola veya anahtar girdiyse
Önce sırrı iptal edin ya da değiştirin (revoke/rotate). Geçmişi yeniden yazmak sızıntıyı geri almaz: sır ekip arkadaşlarınızın klonlarında, fork'larda, CI günlüklerinde ve önbelleklerde kalabilir. GitHub belgeleri, geçmiş yeniden yazılıp zorla gönderilse bile eski commit'lerin klonlarda ve fork'larda, hash ile açılan önbellekli görünümlerde ve onlara referans veren pull request'lerde erişilebilir kalabileceğini söyler; önbellekteki görünümlerin ve pull request referanslarının kaldırılması için GitHub Support'a başvurmak gerekir. Aynı belgeye göre Support bu konuda yalnızca riskin sırrı değiştirerek giderilemediği durumlarda yardımcı olur. Sırları düzenli olarak değiştiren bir süreç kurmak için secret rotation yazımıza bakabilirsiniz. Commit henüz push edilmediyse iş kolaydır: aşağıdaki "Son commit'ten bir dosyayı çıkarmak" adımı yeterlidir.git restore ve git switch: checkout'un iki işi ayrıldı
git checkout birbirinden farklı iki işi tek komutta toplar: dal değiştirmek ve dosya geri yüklemek. Git 2.23 (Ağustos 2019) bu iki iş için ayrı komutlar getirdi: git switch ve git restore. Eski komut çalışmaya devam ediyor; Git 2.51 ile yeni komutların "deneysel" etiketi de kaldırıldı. git --version 2.23'ten eski bir sürüm gösteriyorsa aşağıdaki tablonun eski sütununu kullanın. Varsayılan ayarlarda git status çıktısındaki ipucu satırları da hangi komutun önerildiğini gösterir.| Amaç | Eski komut | Yeni komut |
|---|---|---|
| Dosyayı hazırlama alanından çıkarmak | git reset HEAD DOSYA | git restore --staged DOSYA |
| Dosyadaki değişikliği atmak | git checkout -- DOSYA | git restore DOSYA |
| Eski commit'teki hâli getirip hazırlamak | git checkout HASH -- DOSYA | git restore --source=HASH --staged --worktree DOSYA |
| Dala geçmek | git checkout dal-adi | git switch dal-adi |
| Yeni dal açıp geçmek | git checkout -b yeni-dal | git switch -c yeni-dal |
| Bir commit'e bakmak (ayrık HEAD) | git checkout HASH | git switch --detach HASH |
| Önceki dala dönmek | git checkout - | git switch - |
Hazırlama alanından çıkarmak (unstage)
git add . ile derleme çıktısı da hazırlandıysa:
Kod:
$ git add .
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: app.py
new file: dist/app.js
$ git restore --staged dist/
$ git status --short
M app.py
?? dist/
Klasör yeniden izlenmeyen (??) duruma döndü, dosyalar diskte duruyor. Aynı şeyin tekrar yaşanmaması için dist/ satırını .gitignore dosyasına ekleyin. Henüz hiç commit'i olmayan yeni bir depoda git status bunun yerine git rm --cached önerir; orada HEAD olmadığı için doğru komut odur. Sisteminizin dili Türkçeyse Git aynı ipucunu Türkçe gösterebilir:
Kod:
(hazırlıktan çıkarmak için "git restore --staged <dosya>..." kullanın)
Hata metnini internette aramak için komutu LC_ALL=C git status biçiminde çalıştırıp İngilizce çıktıyı alabilirsiniz.
Değişiklikleri atmak: restore'un kaynağı
git restore DOSYA, dosyayı HEAD'e değil, hazırlama alanındaki hâline döndürür. Değişikliği git add ile hazırladıysanız komut hiçbir şey yapmıyor gibi görünür:
Kod:
$ git add app.py
$ git restore app.py
$ git status --short
M app.py
$ git restore --staged --worktree app.py
$ git status --short
--staged --worktree (kısaca -SW) hem hazırlama alanını hem de diski HEAD'den geri yükler. Bu komutların geri dönüşü yoktur: hiç commit'lenmemiş bir değişiklik Git'te kayıtlı değildir (hazırlanmış içerik için aşağıdaki fsck bölümüne bakın). git restore izlenmeyen dosyalara da dokunmaz. Yeni oluşturulmuş dosyaları silmek için git clean kullanılır; önce mutlaka -n ile hiçbir şey silmeden neyin silineceğini listeleyin (dry run):
Kod:
$ git restore .
$ git status --short
?? build/
?? notes.txt
$ git clean -nd
Would remove build/
Would remove notes.txt
Listeyi kontrol ettikten sonra git clean -fd siler. -x eklerseniz .gitignore'daki dosyalar da (.env, node_modules gibi) gider; bu bayrağı bilerek kullanın.
Bir dosyanın eski hâlini getirmek
Kod:
$ git restore --source=HEAD~1 app.py
$ git status --short
M app.py
$ git restore app.py
$ git checkout HEAD~1 -- app.py
$ git status --short
M app.py
İnce ama önemli bir fark: restore --source yalnızca diski değiştirir (M ikinci sütunda), eski git checkout HASH -- DOSYA ise dosyayı hem diske yazar hem hazırlar (M birinci sütunda). Kaynak commit'te dosya hiç yoksa restore onu siler: örnek depoda git restore --source=HEAD~3 -SW app.py komutundan sonra durum "D app.py" oldu, çünkü app.py ilk commit'te yoktu. Dosyaya hiçbir şeyi değiştirmeden bakmak için git show HEAD~1:app.py yeterlidir.
git switch ve ayrık HEAD
Kod:
$ git switch 560853b
fatal: a branch is expected, got commit '560853b'
hint: If you want to detach HEAD at the commit, try again with the --detach option.
$ git switch --detach 560853b
HEAD is now at 560853b Ürün listesi uç noktası eklendi
$ git switch -
Previous HEAD position was 560853b Ürün listesi uç noktası eklendi
Switched to branch 'main'
git checkout'a bir commit verdiğinizde komut sizi doğrudan ayrık HEAD (detached HEAD) durumuna geçirir ve yalnızca uzun bir uyarı ("You are in 'detached HEAD' state") yazar; git switch ise --detach eklemedikçe bunu reddeder. Ayrık HEAD'deyken commit attıysanız ayrılmadan önce git switch -c yeni-dal ile bu commit'lere bir dal verin; aksi hâlde onlara yalnızca reflog'dan ulaşabilirsiniz.
Reflog ile kurtarma
Reflog, HEAD'in ve her dalın yerel deponuzda nerelere işaret ettiğini kaydeder. reset --hard ile "kaybolan" commit aslında silinmemiştir:
Kod:
$ git reset --hard HEAD~1
HEAD is now at eb201e5 Fiyat filtresi eklendi
$ git reflog -5
eb201e5 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~1
556f46e HEAD@{1}: commit: Hata ayıklama günlüğü eklendi
eb201e5 (HEAD -> main) HEAD@{2}: commit: Fiyat filtresi eklendi
560853b HEAD@{3}: commit: Ürün listesi uç noktası eklendi
7fc6d93 HEAD@{4}: commit (initial): İlk commit
$ git reset --hard HEAD@{1}
HEAD is now at 556f46e Hata ayıklama günlüğü eklendi
HEAD@{1}, "HEAD'in bir önceki konumu" demektir. Daha temkinli yol, hiçbir şeyi ezmeden kayıp commit'e bir dal vermektir: git branch kurtarma HEAD@{1}. Ardından git log kurtarma ile içeriği inceler, isterseniz birleştirir ya da cherry-pick edersiniz. Tek bir dalın geçmişi için git reflog show main, zaman bilgisi için git reflog --date=iso kullanılabilir.
Reflog'un sınırlarını bilin:
- Yereldir: Push edilmez, klonlanmaz. Başka bir bilgisayardaki ya da yeni klonlanmış bir depodaki reflog sizin hareketlerinizi bilmez.
- Süresizdir sanılır ama değildir: git gc (Git bunu zaman zaman kendiliğinden de çalıştırır) varsayılan olarak 90 günden eski girdileri temizler (gc.reflogExpire); dalın güncel ucundan erişilemeyen girdilerde bu süre 30 gündür (gc.reflogExpireUnreachable). Hiçbir yerden erişilemeyen nesneler de varsayılan iki haftalık bekleme süresinden (gc.pruneExpire) sonra silinir.
- Geçmiş temizleme araçları onu sıfırlar: git filter-repo reflog'u bilerek temizler ve git gc çalıştırır; bu tür araçlardan önce yedek alın.
Son çare: git fsck
reset --hard'dan önce git add ile hazırlanmış ama commit'lenmemiş içerik nesne veritabanına yazılmıştır ve çoğu zaman kurtarılabilir; yalnızca dosya adı kaybolur:
Kod:
$ git add notes.txt
$ git reset --hard
HEAD is now at 556f46e Hata ayıklama günlüğü eklendi
$ git fsck --lost-found
dangling blob 0909b591cca24124100575cef96c9138ef65352a
$ cat .git/lost-found/other/0909b591cca24124100575cef96c9138ef65352a
Toplantı notları
--lost-found, sahipsiz (dangling) nesneleri .git/lost-found/commit/ ve .git/lost-found/other/ klasörlerine yazar; blob'ların içeriği doğrudan dosyaya çıkarılır. git fsck --no-reflogs ise yalnızca reflog'da kalmış commit'leri de sahipsiz (dangling commit) olarak listeler; reset ile daldan düşen bir commit'i bu yolla da bulabilirsiniz. git stash drop ya da git stash clear ile silinen bir stash için Git belgelerinin önerdiği tarif şudur:
Bash:
git fsck --unreachable | grep commit | cut -d' ' -f3 | xargs git log --merges --no-walk --grep=WIP
Hiç hazırlanmamış değişiklikler ise Git'te yoktur. Böyle bir durumda editörünüzün yerel geçmişine bakın: VS Code'un Timeline görünümündeki yerel geçmiş (local history) ve JetBrains IDE'lerindeki Local History, Git'in hiç görmediği kayıtlı sürümleri saklayabilir.
Özel durumlar
Yanlış dala commit atmak
main'e atılan commit aslında yeni bir özellik dalına aitse ve main henüz push edilmediyse:
Kod:
$ git branch feature/kampanya
$ git reset --keep HEAD~1
$ git switch feature/kampanya
Switched to branch 'feature/kampanya'
$ git log --oneline -2
8d809b1 (HEAD -> feature/kampanya) Kampanya sayfası eklendi
556f46e (main) Hata ayıklama günlüğü eklendi
Hedef dal zaten varsa commit'i oraya kopyalayıp yanlış daldan çıkarın:
Bash:
git switch feature/kampanya
git cherry-pick HASH # yanlış daldaki commit
git switch main
git reset --keep HEAD~1 # main push edildiyse bunun yerine: git revert HASH
Bir commit'i ikiye bölmek
Bash:
git reset HEAD~1 # commit gider, değişiklikler dosyalarda kalır
git add -p # ilk mantıksal parçayı seçin
git commit -m "Birinci parça"
git add -p
git commit -m "İkinci parça"
Commit yeni dosyalar içeriyorsa Git belgelerinin önerdiği gibi ilk adımda git reset -N HEAD~1 kullanın; -N bu dosyaları git add -p'nin görebileceği şekilde işaretler. Daha eski bir commit'i bölmek için git rebase -i çalıştırıp o commit'in satırında pick yerine edit yazın; rebase durduğunda aynı adımları uygulayıp git rebase --continue çalıştırın.
Amend'i ya da rebase'i geri almak
Kod:
$ git reflog -2
71c87b7 (HEAD -> main) HEAD@{0}: commit (amend): Sayfalama hatası düzeltildi
f74d1ae HEAD@{1}: commit: Sayfalama hatası düzeltildi
$ git reset --soft HEAD@{1}
$ git status --short
A settings.py
Dal amend öncesindeki commit'e döndü; amend sırasında eklenen settings.py hazırlanmış olarak bekliyor. Hatalı bir rebase'i hemen fark ettiyseniz git reset --hard ORIG_HEAD yeterlidir: reset, merge ve rebase gibi HEAD'i büyük ölçüde taşıyan komutlar eski konumu ORIG_HEAD'de saklar. Arada başka bir komut ORIG_HEAD'i değiştirdiyse dalın kendi reflog'una bakın; rebase dalı tek adımda taşıdığı için bir önceki girdi rebase öncesi uçtur:
Kod:
$ git reflog show feature/sepet -2
b985b53 (HEAD -> feature/sepet) feature/sepet@{0}: rebase (finish): refs/heads/feature/sepet onto 556f46e2843f0bbee8c3d0851e2a79d4c51417e3
84e6511 feature/sepet@{1}: commit: Sepet özeti eklendi
$ git reset --hard feature/sepet@{1}
HEAD is now at 84e6511 Sepet özeti eklendi
Birleştirmeyi (merge) geri almak
- Çakışmayla durdu, henüz commit yok: git merge --abort.
- Commit'lendi, push edilmedi: git reset --merge ORIG_HEAD. Birleştirmeyle ilgisi olmayan yerel değişiklikleriniz korunur; çalışma ağacı temizse git reset --hard ORIG_HEAD de iş görür.
- Push edildi: git revert -m 1 MERGE_HASH ve yukarıdaki "revert'ü geri al" tuzağı.
Son commit'ten bir dosyayı çıkarmak, diskte tutmak
Kod:
$ git show --stat --oneline HEAD
eb8b6d3 (HEAD -> main) Ödeme ayarları eklendi
.env | 1 +
payment.py | 1 +
2 files changed, 2 insertions(+)
$ git rm --cached .env
rm '.env'
$ echo ".env" >> .gitignore
$ git add .gitignore
$ git commit -q --amend --no-edit
$ git show --stat --oneline HEAD
46e9f2f (HEAD -> main) Ödeme ayarları eklendi
.gitignore | 1 +
payment.py | 1 +
2 files changed, 2 insertions(+)
.env diskte duruyor ama artık commit'te yok. Commit push edilmediyse bu yeterlidir. Dikkat: git rm --cached, daha önce commit'lenmiş bir dosyada "bu dosyanın depodan silinmesini hazırla" anlamına gelir. Bir dosyayı yalnızca hazırlıktan çıkarmak için git restore --staged kullanın.
Büyük dosyaları veya sırları tüm geçmişten silmek
Git belgeleri git filter-branch için "kullanılması önerilmez" uyarısı taşır ve yerine git filter-repo'yu gösterir. filter-repo tek dosyalık bir Python betiğidir; Homebrew, pipx ve birçok Linux paket yöneticisiyle kurulabilir. Güncel sürüm Git 2.36 ve Python 3.6 ya da üstünü ister.
Bash:
# Her zaman yeni bir klonda çalışın; filter-repo yeni klon gibi görünmeyen bir depoda varsayılan olarak durur
git clone [email protected]:ornek/magaza-api.git magaza-api-temiz
cd magaza-api-temiz
# İhtiyacınıza göre birini seçin
git filter-repo --analyze # büyük dosya ve yol raporu
git filter-repo --strip-blobs-bigger-than 10M # 10 MB'tan büyük blob'ları sil
git filter-repo --invert-paths --path config/secrets.yml # bir dosyayı tüm geçmişten çıkar
git filter-repo --sensitive-data-removal --replace-text ../sifreler.txt # listedeki metinleri tüm geçmişte maskele
Bilmeniz gerekenler: filter-repo değişen commit'lerin ve onlardan sonraki tüm commit'lerin hash'ini değiştirir, ardından reflog'u temizleyip git gc çalıştırır; yerelde geri dönüş yoktur. Normal modda, eski depoya yanlışlıkla geri göndermeyin diye origin tanımını kaldırır. GitHub'ın sır temizliği için önerdiği --sensitive-data-removal bayrağı (filter-repo 2.47 ve sonrası) ise origin'i korur, önce oradaki bütün referansları getirir ve sonunda kalan temizlik adımlarını raporlar. Sır temizliğinde sonuç, GitHub belgelerindeki gibi git push --force --mirror origin ile gönderilir: korumalı dallarda korumayı geçici olarak kapatmanız gerekir, GitHub'ın salt okunur tuttuğu refs/pull/ referansları ise push edilemez. Normal modda origin kaldırıldığı için aynı depoya göndermek isterseniz önce git remote add origin URL ile onu yeniden eklemeniz gerekir; filter-repo belgeleri mümkünse yeni bir depoya göndermeyi önerir. Ekipteki herkes eski klonunu silip yeniden klonlamalıdır; eski bir klondan yapılacak tek bir git pull ve git push, temizlenen veriyi geri getirebilir.
Grafik arayüzlerde aynı işlemler: VS Code, GitHub Desktop, JetBrains
VS Code'un "Git: Undo Last Commit" komutu git reset --soft HEAD~ çalıştırır ve eski mesajı commit kutusuna geri koyar; Kaynak Denetimi (Source Control) görünümündeki "Discard Changes" restore'un karşılığıdır (izlenmeyen dosyaları ise varsayılan olarak çöp kutusuna taşır); "Unstage Changes" komutu ise restore --staged'in karşılığıdır. VS Code'da zorla gönderme varsayılan olarak kapalıdır (git.allowForcePush); açtığınızda varsayılan ayarlar --force-with-lease ve --force-if-includes kullanır. GitHub Desktop'ta commit'ten sonra Changes sekmesinin altında Undo düğmesi çıkar; History sekmesinde bir commit'e sağ tıklayınca Amend commit, Reset to commit (en son push edilen commit'e kadar) ve Revert Changes in Commit seçenekleri bulunur. IntelliJ IDEA, PyCharm ve WebStorm gibi JetBrains IDE'lerinde Git penceresinin Log sekmesindeki sağ tık menüsü Undo Commit, Revert Commit, Drop Commit ve Soft, Mixed, Hard, Keep modlarını sunan Reset Current Branch to Here seçeneklerini içerir; commit'lenmemiş değişiklikler için Rollback kullanılır. Bu düğmelerin hepsi arka planda bu yazıdaki Git işlemlerini yapar; tıklamadan önce hangisine karşılık geldiğini bilmek sürprizleri önler.Sık yapılan hatalar
- Commit'lenmemiş iş varken reset --hard: Önce git status; gerekiyorsa git stash push -u ya da --keep.
- main'e zorla göndermek: Paylaşılan dalda revert kullanın, dal korumasını açık tutun.
- Birleştirmeyi -m olmadan geri almaya çalışmak: "is a merge but no -m option was given" hatası alırsınız; -m 1 kullanın ve yeniden birleştirmeden önce revert'ü geri almayı unutmayın.
- revert'ün veriyi sildiğini sanmak: revert yalnızca ters bir değişiklik ekler; eski içerik geçmişte durur. Sızan bir sır revert ile gizlenmez, iptal edilmesi gerekir.
- Push edilmiş commit'i amend etmek: Dal ayrışır; ekip arkadaşlarınız merge ile çekerse aynı değişiklik geçmişte iki kez görünür. Ek değişiklikleri yeni bir commit olarak gönderin.
Sık sorulan sorular
git reset ile git revert arasındaki fark nedir?git reset dal ucunu geriye taşır ve commit'leri dalın geçmişinden çıkarır; push edilmemiş commit'ler ya da yalnızca sizin kullandığınız dallar için uygundur. git revert ise geçmişi değiştirmez, hatalı commit'in tersini yeni bir commit olarak ekler; paylaşılan dallarda doğru araç budur.
Push ettiğim son commit'i nasıl geri alırım?
Dal paylaşılıyorsa git revert HEAD ve ardından normal bir git push. Dal yalnızca sizinse commit'i reset ile geri alıp git push --force-with-lease --force-if-includes kullanabilirsiniz; korumalı dallar bunu reddeder.
git reset --hard yaptım, değişikliklerim geri gelir mi?
Commit'lenmiş değişiklikler git reflog ile geri gelir. Hazırlanmış ama commit'lenmemiş içerik çoğu zaman git fsck --lost-found ile kurtarılır. Hiç hazırlanmamış değişiklikler Git'te yoktur; editörünüzün yerel geçmişine bakın.
HEAD~1 ile HEAD^ aynı şey mi?
Evet, ikisi de ilk ebeveyni gösterir. Fark sayılarla ortaya çıkar: HEAD~2 iki kuşak geridir, HEAD^2 ise bir birleştirme commit'inin ikinci ebeveynidir.
git restore ile git checkout arasındaki fark ne?
git restore yalnızca dosyaları geri yükler, git switch yalnızca dal değiştirir; git checkout ise ikisini birden yapar. Pratikteki fark şu: checkout HASH -- DOSYA dosyayı hazırlar, restore --source=HASH DOSYA ise yalnızca diske yazar.
Sonuç ve sonraki adımlar
Kısa özet: push edilmediyse reset ya da amend, push edildiyse revert; dosya düzeyinde restore; bir şey kaybolduysa önce reflog. Bu kararları ekipte alışkanlığa dönüştürmek için üç adım öneriyoruz. Birincisi, sık kullanılan komutlara kısa yollar tanımlamak ve zorla göndermeyi güvenli varsayılana bağlamak:
Bash:
git config --global alias.undo "reset --soft HEAD~1"
git config --global alias.unstage "restore --staged --"
git config --global alias.pushf "push --force-with-lease --force-if-includes"
git config --global push.useForceIfIncludes true
İkincisi, GitHub ya da GitLab'da main için dal korumasını açık tutmak: zorla gönderme ve dal silme kapalı olsun, birleştirmeden önce CI kontrollerinin geçmesi zorunlu olsun. CI hattının genel yapısı için CI/CD pipeline yazımıza, testlerin hatta nasıl dizileceği için CI test tasarımı yazımıza bakabilirsiniz. Üçüncüsü, sorunları daha commit aşamasında yakalayan pre-commit kancaları kurmak:
YAML:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-added-large-files
- id: detect-private-key
- id: no-commit-to-branch
args: [--branch, main]
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaks
pre-commit aracını kurduktan sonra (örneğin pipx install pre-commit) pre-commit install komutu kancaları depoya bağlar, pre-commit autoupdate ise rev değerlerini güncel tutar. Makinesinde gitleaks kurulu olanlar gitleaks yerine gitleaks-system kancasını kullanabilir. Ekibinizin Git akışını, dal korumalarını ve CI hattını birlikte kurgulamak isterseniz AIOR'un yazılım mühendisliği hizmetleri de bir seçenek. Takıldığınız bir durumu git status ve git log --oneline --graph -10 çıktılarıyla birlikte bu konuya yazabilirsiniz.
There is no single "undo" button in Git. The right command depends on two questions: has the commit left your machine (pushed, or pulled by someone else), and do you want to keep the changes it contains? This guide maps the answers to git reset, git revert, git restore and git commit --amend, shows how to recover with the reflog when an undo goes wrong, and explains what changes once the commit is on GitHub or GitLab. Every output below was reproduced in October 2026 with Git 2.54 in a sample repository called shop-api. Your commit hashes will be different.
The 30-second decision table
| Situation | Command | If the commit is pushed |
|---|---|---|
| Fix the last commit message or add a forgotten file | git commit --amend | Avoid: it rewrites history |
| Undo the last local commit, keep the changes staged | git reset --soft HEAD~1 | Avoid |
| Undo the last local commit, keep the changes as unstaged edits | git reset HEAD~1 (default mode --mixed) | Avoid |
| Throw away the commit and everything in it | git reset --hard HEAD~1 (dangerous; --keep is safer) | Avoid |
| Undo a commit that is already pushed or shared | git revert HASH | The right tool |
| Undo a pushed merge | git revert -m 1 MERGE_HASH | The right tool |
| Unstage a file | git restore --staged FILE | Doesn't touch commits |
| Discard uncommitted edits in a file | git restore FILE (cannot be undone) | Doesn't touch commits |
| Get a file back as it was in an older commit | git restore --source=HASH FILE | Doesn't touch commits |
| Find a commit lost to a bad reset, rebase or amend | git reflog, then git branch rescue HASH | Works in your local repository only |
| Back out of a merge that stopped on conflicts | git merge --abort | Nothing committed yet |
Has it been pushed?
This is the column that matters most, and you don't have to guess:
Kod:
$ git status
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
(use "git push" to publish your local commits)
nothing to commit, working tree clean
$ git log --oneline origin/main..HEAD
fbc92f4 (HEAD -> main) Fix pagnation bug
"ahead of 'origin/main' by 1 commit" means one commit hasn't been sent yet, and the origin/main..HEAD range lists exactly which ones. git branch -r --contains HASH prints the remote branches that contain a commit: in our test it printed nothing before the push and origin/main after it. All three commands work from what your last git fetch saw, so run git fetch first if you need to be sure.
The mental model: three areas and a pointer
A file can exist in three versions at once: the working tree (the files on disk you edit), the index or staging area (what git add has prepared for the next commit) and the committed history. HEAD marks where you are: normally it points to a branch, and the branch points to a commit. Every undo command changes one of these areas or moves the branch pointer:
Kod:
Working tree Index Commit history
(files on disk) (staging area)
app.py --- git add -------→ app.py --- git commit --→ f3815da ← main ← HEAD
←-- git restore ---- ←-- git restore --staged --
The difference between reset and revert shows up in the history:
Kod:
git reset HEAD~1 → the branch tip moves back; history is rewritten
before: A---B---C---D main
after : A---B---C main (D is on no branch, but stays in the reflog)
git revert D → a new commit that undoes D is added; history is kept
before: A---B---C---D main
after : A---B---C---D---D' main (D' = the inverse of D's changes)
| Command | Branch tip (HEAD) | Index | Working tree |
|---|---|---|---|
| git reset --soft X | Moves to X | Unchanged | Unchanged |
| git reset --mixed X (default) | Moves to X | Reset to X | Unchanged |
| git reset --hard X | Moves to X | Reset to X | Reset to X; uncommitted changes to tracked files are lost |
| git reset --keep X | Moves to X | Reset to X | Only files that differ between HEAD and X are updated; aborts if any of those have local changes |
| git revert X | A new commit is added | Updated | Updated |
| git restore FILE | Unchanged | Unchanged | File goes back to its staged version |
| git restore --staged FILE | Unchanged | File goes back to its HEAD version | Unchanged |
git reset in depth: --soft, --mixed, --hard and --keep
This is the sample repository. We want to get rid of the debug logging added by the last commit:
Kod:
$ git log --oneline
f3815da (HEAD -> main) Add debug logging
42c5d11 Add price filter
65f652b Add product list endpoint
3697fb8 Initial commit
HEAD~1, HEAD^ and hashes
- HEAD~1 and HEAD^ name the same commit: the first parent of HEAD. HEAD~3 goes three generations back, following first parents.
- HEAD^2 is something else: the second parent of a merge commit, which is the tip of the branch that was merged in.
- A hash is the most explicit option: git reset --soft 42c5d11. An abbreviated hash works as long as it is unique in the repository.
- In Windows cmd.exe, ^ is the escape character, and zsh with the extendedglob option answers HEAD^ with "no matches found: HEAD^". Typing HEAD~1 avoids both problems.
--soft: the commit goes, the changes stay staged
Kod:
$ git reset --soft HEAD~1
$ git log --oneline
42c5d11 (HEAD -> main) Add price filter
65f652b Add product list endpoint
3697fb8 Initial commit
$ git status --short
M app.py
--mixed (the default): the commit goes, the changes stay in your files
Kod:
$ git reset HEAD~1
Unstaged changes after reset:
M app.py
$ git status --short
M app.py
--hard: the commit and its changes are gone
Kod:
$ git reset --hard HEAD~1
HEAD is now at 42c5d11 Add price filter
$ git status --short
--keep: the careful sibling of --hard
--keep drops commits like --hard but refuses to overwrite your local edits. If a file changed by the commits being dropped also has local changes, it stops without touching anything:
Kod:
$ git reset --keep HEAD~1 # try 1: local edit in README.md
$ git status --short
M README.md
$ git reset --keep HEAD~1 # try 2, same starting point: local edit in app.py, which the dropped commit also changed
error: Entry 'app.py' not uptodate. Cannot merge.
fatal: Could not reset index file to revision 'HEAD~1'.
Undoing the very first commit
The first (root) commit has no parent, so HEAD~1 doesn't resolve:
Kod:
$ git reset --soft HEAD~1
fatal: ambiguous argument 'HEAD~1': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'
$ git update-ref -d HEAD
$ git status --short
A README.md
git commit --amend: fixing the last commit
A typo in the message or a forgotten file doesn't call for reset at all:
Kod:
$ git commit -m "Fix pagnation bug"
[main ddb6c45] Fix pagnation bug
1 file changed, 1 insertion(+)
create mode 100644 pagination.py
$ git commit --amend -m "Fix pagination bug"
[main 32017fa] Fix pagination bug
Date: Tue Oct 20 10:05:00 2026 +0300
1 file changed, 1 insertion(+)
create mode 100644 pagination.py
Note the new hash: ddb6c45 is gone and 32017fa took its place. Amend doesn't edit a commit; it replaces it. For a forgotten file, run git add FILE and then git commit --amend --no-edit, which keeps the message as it is. To reword an older commit, run git rebase -i HEAD~3 and change pick to reword on its line. Both rewrite history, so if the commit is already pushed, read "If the commit is already pushed" below first.
git revert: the safe undo for shared branches
git revert creates a new commit that applies the inverse of the target commit. Nothing is deleted and nobody's clone breaks, which is why it is the right tool for shared branches such as main. It needs a clean working tree.
Kod:
$ git revert --no-edit HEAD~1
[main 41840c3] Revert "Add price filter"
Date: Tue Oct 20 10:10:00 2026 +0300
1 file changed, 2 deletions(-)
delete mode 100644 filters.py
$ git log --oneline -4
41840c3 (HEAD -> main) Revert "Add price filter"
f3815da Add debug logging
42c5d11 Add price filter
65f652b Add product list endpoint
$ git show -s --format=%B HEAD
Revert "Add price filter"
This reverts commit 42c5d119fb3909553538946dfa1ce9bf1b026577.
We used --no-edit only to keep the output short. In a real project, let the editor open and write down why you are reverting; the Git documentation strongly recommends it. For several commits:
Bash:
# One revert commit per original commit (newest first)
git revert --no-edit HEAD~2..HEAD
# The same undo as a single commit
git revert --no-commit HEAD~2..HEAD
git commit -m "Revert price filter and debug logging"
A range A..B excludes A, so HEAD~2..HEAD reverts HEAD~1 and HEAD. If a revert stops on a conflict, fix the files, mark them with git add and run git revert --continue; git revert --abort cancels the whole operation. On GitHub, the Revert button at the bottom of a merged pull request opens a new pull request that reverts its merge commit; GitLab has a Revert button on merged merge requests that does the same job.
Reverting a merge commit: -m 1
A merge commit has two parents, and Git won't guess which side you want to keep:
Kod:
$ git log --oneline --graph -6
* b32603d (HEAD -> main) Merge branch 'feature/discount'
|\
| * 19efd10 (feature/discount) Show discount at checkout
| * 8308bd4 Add discount rules
|/
* f3815da Add debug logging
* 42c5d11 Add price filter
* 65f652b Add product list endpoint
$ git revert --no-edit HEAD
error: commit b32603da185be0d8a36f696fe6bd5a24e981f286 is a merge but no -m option was given.
fatal: revert failed
$ git log -1 --format='%h %p' HEAD
b32603d f3815da 19efd10
$ git revert -m 1 --no-edit HEAD
[main e58933d] Revert "Merge branch 'feature/discount'"
Date: Tue Oct 20 10:17:00 2026 +0300
2 files changed, 4 deletions(-)
delete mode 100644 checkout.py
delete mode 100644 discount.py
%p prints the parents in order: parent 1 is the branch you merged into (main, f3815da here) and parent 2 is the tip of the branch that was merged (19efd10). -m 1 says "the mainline is the first parent; undo what came in from the other side". For branches merged through a pull request or merge request, 1 is almost always the right number.
The "revert the revert" trap
Reverting a merge undoes the changes to the files, not the history. Git still considers the branch's commits merged, so when the team fixes the branch and merges it again, only the new commits arrive:
Kod:
# one more commit was added to feature/discount (tests/test_discount.py)
$ git merge --no-edit feature/discount
Merge made by the 'ort' strategy.
tests/test_discount.py | 1 +
1 file changed, 1 insertion(+)
create mode 100644 tests/test_discount.py
$ ls
README.md
app.py
filters.py
tests
The merge looks successful, yet discount.py and checkout.py are missing. The correct order is to revert the revert commit before merging again. Since Git 2.43, such a commit gets a "Reapply" subject automatically:
Kod:
$ git revert --no-edit e58933d
[main 634147b] Reapply "Merge branch 'feature/discount'"
Date: Tue Oct 20 10:20:00 2026 +0300
2 files changed, 4 insertions(+)
create mode 100644 checkout.py
create mode 100644 discount.py
$ git merge --no-edit feature/discount
If you have already merged a second time, reverting the revert still brings the missing files back; it did in our test. One important exception: if the team threw away the faulty commits and rebuilt the branch from scratch on top of the current main (a rebase), don't revert the revert; merge the rebuilt branch as it is. Reverting the revert would bring the old, faulty changes back next to their rebuilt versions, which usually ends in conflicts. Git's "revert-a-faulty-merge" how-to walks through both scenarios.
If the commit is already pushed
The rule is simple: don't rewrite the history of a branch other people have pulled without talking to them first; use revert. The cases that justify a rewrite are few: a password or key that got committed, commits pushed to the wrong branch, a huge file bloating the repository, or a feature branch only you work on.Amend a pushed commit and your local branch diverges from the remote one, so a normal push is rejected. If you undid it with reset instead, git status reports that your branch is behind, and the push fails with the same non-fast-forward error:
Kod:
$ git status
On branch main
Your branch and 'origin/main' have diverged,
and have 1 and 1 different commits each, respectively.
(use "git pull" if you want to integrate the remote branch with yours)
nothing to commit, working tree clean
$ git push
To github.com:example/shop-api.git
! [rejected] main -> main (non-fast-forward)
error: failed to push some refs to 'github.com:example/shop-api.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
The git pull hint is misleading here: you don't want to integrate the remote commit, you want to replace it. In our test, in a repository with no pull strategy configured, git pull stopped with "fatal: Need to specify how to reconcile divergent branches."; with a strategy configured, the same change can end up in history twice. What to do depends on the branch:
- Shared branch: back out of the amend. Right after amending, git reset --soft HEAD@{1} moves the branch back to the commit you pushed (check git reflog -2 first). Whatever the amend added stays staged; commit it as a new commit and push normally. If you only changed the message, nothing is left over and the typo stays in history, which most teams can live with.
- Branch only you use (for example the branch of an open pull request): git push --force-with-lease --force-if-includes.
--force-with-lease and --force-if-includes
--force overwrites whatever the remote branch points to, including commits a teammate pushed after your last fetch. --force-with-lease only goes ahead if the remote branch is still where you last saw it, that is, where your remote-tracking branch origin/main points:
Kod:
$ git push --force-with-lease
To github.com:example/shop-api.git
! [rejected] main -> main (stale info)
error: failed to push some refs to 'github.com:example/shop-api.git'
"stale info" means someone pushed after your last fetch. The weak spot: if your IDE or a scheduled job runs git fetch in the background, origin/main moves and the lease is satisfied even though you never looked at your teammate's commit:
Kod:
$ git fetch -q # like a background fetch
$ git push --force-with-lease --dry-run
To github.com:example/shop-api.git
+ 7935687...d650f09 main -> main (forced update)
$ git push --force-with-lease --force-if-includes
To github.com:example/shop-api.git
! [rejected] main -> main (remote ref updated since checkout)
error: failed to push some refs to 'github.com:example/shop-api.git'
hint: Updates were rejected because the tip of the remote-tracking branch has
hint: been updated since the last checkout. If you want to integrate the
hint: remote changes, use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
The dry run shows the push would have gone through and wiped out the teammate's commit 7935687. --force-if-includes, added in Git 2.30, checks the local branch's reflog to confirm that the tip of the remote-tracking branch has actually been integrated, and refuses the push if it hasn't. To make it the default, run git config --global push.useForceIfIncludes true; the setting only takes effect together with --force-with-lease and does nothing for a plain --force. Tell the team before you force-push. Anyone who uses the rewritten branch should update it with a rebase, not a merge (git pull --rebase), or the old commits come back.
Protected branches are the second safety net. According to GitHub's documentation, every branch protection rule disables force pushes to matching branches by default. On GitLab, the default branch of a project is protected from the start, and force pushes are rejected unless "Allowed to force push" is turned on. If a force push to main is rejected, nothing is broken: the protection is doing its job, and revert is the way forward.
If a password or key was committed
Revoke or rotate the secret first. Rewriting history does not take a leak back: the secret may live on in teammates' clones, in forks, in CI logs and in caches. GitHub's documentation notes that even after a rewrite and force push, the old commits may remain reachable in clones and forks, through cached views opened by hash, and through pull requests that reference them; removing the cached views and pull request references requires contacting GitHub Support, which, according to the same page, only helps when rotating the affected credentials cannot mitigate the risk. To set up a regular process for replacing secrets, see our secret rotation article. If the commit hasn't been pushed yet, the fix is easy: the "Remove a file from the last commit" steps below are enough.git restore and git switch: checkout's two jobs, split apart
git checkout does two unrelated jobs: switching branches and restoring files. Git 2.23 (August 2019) added a separate command for each, git switch and git restore. The old command still works as before, and Git 2.51 dropped the "experimental" label from the new commands. If git --version reports something older than 2.23, use the left-hand column of the table below. With default settings, the hint lines in git status also tell you which command to use.| Goal | Old command | New command |
|---|---|---|
| Unstage a file | git reset HEAD FILE | git restore --staged FILE |
| Discard edits in a file | git checkout -- FILE | git restore FILE |
| Take a file from an old commit and stage it | git checkout HASH -- FILE | git restore --source=HASH --staged --worktree FILE |
| Switch to a branch | git checkout branch-name | git switch branch-name |
| Create a branch and switch to it | git checkout -b new-branch | git switch -c new-branch |
| Look at a commit (detached HEAD) | git checkout HASH | git switch --detach HASH |
| Go back to the previous branch | git checkout - | git switch - |
Unstaging files
When git add . has also picked up build output:
Kod:
$ git add .
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: app.py
new file: dist/app.js
$ git restore --staged dist/
$ git status --short
M app.py
?? dist/
The folder is untracked (??) again and the files are still on disk. Add dist/ to .gitignore so it doesn't happen again. In a brand-new repository with no commits, git status suggests git rm --cached instead, which is correct there because there is no HEAD yet. If your system language isn't English, Git may print translated messages; running LC_ALL=C git status shows the English text, which is what you will find when you search for an error.
Discarding changes: where restore takes the file from
git restore FILE puts the file back to its staged version, not to HEAD. If you already ran git add, the command appears to do nothing:
Kod:
$ git add app.py
$ git restore app.py
$ git status --short
M app.py
$ git restore --staged --worktree app.py
$ git status --short
--staged --worktree (-SW for short) restores both the index and the file on disk from HEAD. None of this can be undone: a change that was never committed isn't recorded in Git (for staged content, see the fsck section below). git restore also leaves untracked files alone. New files are removed with git clean; always start with -n, which only lists what would be deleted (a dry run):
Kod:
$ git restore .
$ git status --short
?? build/
?? notes.txt
$ git clean -nd
Would remove build/
Would remove notes.txt
Once the list looks right, git clean -fd deletes them. Adding -x also removes ignored files such as .env or node_modules, so use that flag deliberately.
Getting an older version of a file
Kod:
$ git restore --source=HEAD~1 app.py
$ git status --short
M app.py
$ git restore app.py
$ git checkout HEAD~1 -- app.py
$ git status --short
M app.py
A small but real difference: restore --source changes only the file on disk (M in the second column), while the old git checkout HASH -- FILE writes the file and stages it (M in the first column). If the file doesn't exist in the source commit, restore deletes it: in the sample repository, git restore --source=HEAD~3 -SW app.py left the status at "D app.py", because app.py wasn't in the first commit. To look at an old version without changing anything, git show HEAD~1:app.py is enough.
git switch and detached HEAD
Kod:
$ git switch 65f652b
fatal: a branch is expected, got commit '65f652b'
hint: If you want to detach HEAD at the commit, try again with the --detach option.
$ git switch --detach 65f652b
HEAD is now at 65f652b Add product list endpoint
$ git switch -
Previous HEAD position was 65f652b Add product list endpoint
Switched to branch 'main'
Give git checkout a commit and it moves you straight into detached HEAD state, printing a long "You are in 'detached HEAD' state" notice; git switch refuses unless you add --detach. If you committed while detached, give those commits a branch with git switch -c new-branch before you leave, or the reflog becomes the only way to reach them.
Recovering with git reflog
The reflog records where HEAD and each branch pointed in your local repository. A commit "lost" to reset --hard hasn't actually been deleted:
Kod:
$ git reset --hard HEAD~1
HEAD is now at 42c5d11 Add price filter
$ git reflog -5
42c5d11 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~1
f3815da HEAD@{1}: commit: Add debug logging
42c5d11 (HEAD -> main) HEAD@{2}: commit: Add price filter
65f652b HEAD@{3}: commit: Add product list endpoint
3697fb8 HEAD@{4}: commit (initial): Initial commit
$ git reset --hard HEAD@{1}
HEAD is now at f3815da Add debug logging
HEAD@{1} means "where HEAD was one move ago". The more careful option overwrites nothing and simply gives the lost commit a branch: git branch rescue HEAD@{1}. Then inspect it with git log rescue and merge or cherry-pick what you need. git reflog show main shows a single branch's history, and git reflog --date=iso adds timestamps.
Know the reflog's limits:
- It is local. It isn't pushed or cloned. A reflog on another machine or in a fresh clone knows nothing about your moves.
- It expires. When git gc runs (Git also starts it automatically from time to time), it removes entries older than 90 days by default (gc.reflogExpire), and entries no longer reachable from the branch's current tip once they are older than 30 days (gc.reflogExpireUnreachable). Objects that nothing references any more are pruned after a default grace period of two weeks (gc.pruneExpire).
- History-cleaning tools wipe it. git filter-repo deliberately expires the reflog and runs git gc, so make a backup before you use such tools.
Last resort: git fsck
Content you staged with git add but never committed before a reset --hard was written to the object database, and you can often get it back. Only the file name is lost:
Kod:
$ git add notes.txt
$ git reset --hard
HEAD is now at f3815da Add debug logging
$ git fsck --lost-found
dangling blob 650165632dda709cdc0a8235796aaaf2e70571c4
$ cat .git/lost-found/other/650165632dda709cdc0a8235796aaaf2e70571c4
Meeting notes
--lost-found writes dangling objects into .git/lost-found/commit/ and .git/lost-found/other/, and for blobs it writes the content itself into the file. git fsck --no-reflogs also reports commits that only the reflog still references as dangling commits, which is another way to find a commit that a reset dropped from its branch. For a stash removed with git stash drop or git stash clear, the Git documentation gives this recipe:
Bash:
git fsck --unreachable | grep commit | cut -d' ' -f3 | xargs git log --merges --no-walk --grep=WIP
Changes that were never staged don't exist in Git at all. In that case, check your editor: the local history in VS Code's Timeline view and Local History in JetBrains IDEs can keep saved versions that Git never saw.
Special cases
Committed to the wrong branch
The commit on main really belongs on a new feature branch, and main hasn't been pushed:
Kod:
$ git branch feature/campaign
$ git reset --keep HEAD~1
$ git switch feature/campaign
Switched to branch 'feature/campaign'
$ git log --oneline -2
dd4777b (HEAD -> feature/campaign) Add campaign page
f3815da (main) Add debug logging
If the target branch already exists, copy the commit there and remove it from the wrong one:
Bash:
git switch feature/campaign
git cherry-pick HASH # the commit on the wrong branch
git switch main
git reset --keep HEAD~1 # if main is already pushed, use git revert HASH instead
Splitting a commit
Bash:
git reset HEAD~1 # the commit goes, the changes stay in your files
git add -p # pick the first logical piece
git commit -m "First part"
git add -p
git commit -m "Second part"
If the commit added new files, follow the Git documentation and start with git reset -N HEAD~1; -N marks those files so that git add -p can see them. To split an older commit, mark its line as edit in git rebase -i, repeat the steps above when the rebase stops, then run git rebase --continue.
Undoing an amend or a rebase
Kod:
$ git reflog -2
696fd4e (HEAD -> main) HEAD@{0}: commit (amend): Fix pagination bug
68ca67b HEAD@{1}: commit: Fix pagination bug
$ git reset --soft HEAD@{1}
$ git status --short
A settings.py
The branch is back on the commit from before the amend, and settings.py, which the amend added, waits in the index. If you notice a bad rebase straight away, git reset --hard ORIG_HEAD is enough: commands that move HEAD drastically, such as reset, merge and rebase, store the previous position in ORIG_HEAD. If something else has overwritten ORIG_HEAD since, use the branch's own reflog. A rebase moves the branch in one step, so the previous entry is the tip from before the rebase:
Kod:
$ git reflog show feature/cart -2
78ecfdd (HEAD -> feature/cart) feature/cart@{0}: rebase (finish): refs/heads/feature/cart onto f3815dab5ba72eee1103f4803a44028c0a5057fc
089c225 feature/cart@{1}: commit: Add cart summary
$ git reset --hard feature/cart@{1}
HEAD is now at 089c225 Add cart summary
Undoing a merge
- Stopped on conflicts, nothing committed: git merge --abort.
- Committed but not pushed: git reset --merge ORIG_HEAD, which keeps local changes unrelated to the merge; with a clean working tree, git reset --hard ORIG_HEAD works too.
- Pushed: git revert -m 1 MERGE_HASH, and mind the "revert the revert" trap above.
Remove a file from the last commit but keep it on disk
Kod:
$ git show --stat --oneline HEAD
3d39849 (HEAD -> main) Add payment settings
.env | 1 +
payment.py | 1 +
2 files changed, 2 insertions(+)
$ git rm --cached .env
rm '.env'
$ echo ".env" >> .gitignore
$ git add .gitignore
$ git commit -q --amend --no-edit
$ git show --stat --oneline HEAD
2006127 (HEAD -> main) Add payment settings
.gitignore | 1 +
payment.py | 1 +
2 files changed, 2 insertions(+)
.env is still on disk but no longer in the commit. If the commit hasn't been pushed, that's all you need. Careful: on a file that is already committed, git rm --cached means "stage the removal of this file from the repository". To merely unstage a file, use git restore --staged.
Purging large files or secrets from all history
The Git documentation warns that git filter-branch is not recommended and points to git filter-repo instead. filter-repo is a single Python script you can install with Homebrew, pipx or most Linux package managers; the current version needs Git 2.36 and Python 3.6 or newer.
Bash:
# Always work in a fresh clone; by default filter-repo refuses to run in a repo that doesn't look like one
git clone [email protected]:example/shop-api.git shop-api-clean
cd shop-api-clean
# Pick the one you need
git filter-repo --analyze # report of large files and paths
git filter-repo --strip-blobs-bigger-than 10M # drop blobs larger than 10 MB
git filter-repo --invert-paths --path config/secrets.yml # remove one file from all history
git filter-repo --sensitive-data-removal --replace-text ../passwords.txt # mask the listed strings throughout history
What to expect: filter-repo changes the hash of every rewritten commit and of all commits after it, then expires the reflog and runs git gc, so there is no local way back. In normal mode it removes the origin remote so you can't push the rewrite back to the old repository by accident. The --sensitive-data-removal flag that GitHub recommends for secrets (filter-repo 2.47 or newer) keeps origin instead, fetches every ref from it first and finishes with a report of the cleanup steps still to do. For a secret cleanup, GitHub's documentation pushes the result with git push --force --mirror origin: protected branches need their protection lifted temporarily, and the refs/pull/ refs that GitHub keeps read-only cannot be pushed. In normal mode origin is gone, so to push back to the same repository you first re-add it with git remote add origin URL; the filter-repo documentation suggests pushing to a new repository where possible. Everyone on the team should delete their old clone and clone again; a single git pull and git push from an old clone can bring the purged data back.
The same operations in GUI tools: VS Code, GitHub Desktop, JetBrains
VS Code's "Git: Undo Last Commit" command runs git reset --soft HEAD~ and puts the old message back in the commit box; in the Source Control view, "Discard Changes" corresponds to restore (untracked files are moved to the trash by default) and "Unstage Changes" to restore --staged. Force pushing is off by default in VS Code (git.allowForcePush), and once you enable it, the default settings use --force-with-lease and --force-if-includes. In GitHub Desktop, an Undo button appears at the bottom of the Changes tab after you commit; right-clicking a commit in the History tab offers Amend commit, Reset to commit (back as far as the most recently pushed commit) and Revert Changes in Commit. In JetBrains IDEs such as IntelliJ IDEA, PyCharm and WebStorm, the context menu in the Log tab of the Git tool window has Undo Commit, Revert Commit, Drop Commit and Reset Current Branch to Here with Soft, Mixed, Hard and Keep modes, while Rollback discards uncommitted changes. All of these buttons perform the Git operations described in this guide, and knowing which one you are about to trigger avoids surprises.Common mistakes
- reset --hard with uncommitted work: Run git status first; use git stash push -u or --keep when in doubt.
- Force-pushing main: Use revert on shared branches and keep branch protection on.
- Reverting a merge without -m: You get "is a merge but no -m option was given". Use -m 1, and remember to revert the revert before merging the branch again.
- Believing revert deletes data: revert only adds an inverse change; the old content stays in history. A leaked secret isn't hidden by a revert; it has to be revoked.
- Amending a pushed commit: The branch diverges, and if teammates pull with a merge, the same change shows up twice. Send follow-up changes as a new commit.
FAQ
What is the difference between git reset and git revert?git reset moves the branch tip back and takes commits out of the branch's history, so it suits commits that haven't been pushed, or branches only you use. git revert leaves history alone and adds a new commit that applies the inverse of a bad commit, which makes it the right tool for shared branches.
How do I undo the last commit I already pushed?
On a shared branch, git revert HEAD followed by a normal git push. On a branch only you use, you can reset the commit and push with git push --force-with-lease --force-if-includes; protected branches will reject it.
I ran git reset --hard. Can I get my changes back?
Committed changes come back through git reflog. Staged but uncommitted content can often be recovered with git fsck --lost-found. Changes that were never staged aren't in Git; check your editor's local history.
Are HEAD~1 and HEAD^ the same?
Yes, both name the first parent. The difference appears with numbers: HEAD~2 is two generations back, while HEAD^2 is the second parent of a merge commit.
What is the difference between git restore and git checkout?
git restore only restores files and git switch only changes branches; git checkout does both. The practical difference: checkout HASH -- FILE stages the file, while restore --source=HASH FILE only writes it to disk.
Conclusion and next steps
In short: not pushed yet, use reset or amend; already pushed, use revert; file-level fixes, use restore; something lost, check the reflog first. Three steps turn those decisions into team habits. First, add shortcuts for the commands you use most and make the safer force push the default:
Bash:
git config --global alias.undo "reset --soft HEAD~1"
git config --global alias.unstage "restore --staged --"
git config --global alias.pushf "push --force-with-lease --force-if-includes"
git config --global push.useForceIfIncludes true
Second, keep branch protection on for main in GitHub or GitLab: no force pushes, no deletion, and required CI checks before merging. Our article on CI/CD pipelines covers the overall pipeline, and our CI test pipeline design article covers how to order the test stages. Third, install pre-commit hooks that catch problems before they become commits:
YAML:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-added-large-files
- id: detect-private-key
- id: no-commit-to-branch
args: [--branch, main]
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaks
Once the pre-commit tool is installed (for example with pipx install pre-commit), pre-commit install wires the hooks into the repository, and pre-commit autoupdate keeps the rev values current. If gitleaks is already installed on your machine, you can use the gitleaks-system hook instead of gitleaks. If you would like help designing your team's Git workflow, branch protection and CI pipeline together, AIOR's software engineering services are one option. If you get stuck, post your situation in this thread along with the output of git status and git log --oneline --graph -10.