Split View: Git 되돌리기 완전 가이드: 복구되는 것과 영원히 사라지는 것
Git 되돌리기 완전 가이드: 복구되는 것과 영원히 사라지는 것
- 들어가며
- 1. 복구 가능성 지도 — 이 글의 핵심 한 장
- 2. 세 개의 영역과 그것을 옮기는 명령
- 3. reset — 세 가지 모드의 실제 차이
- 4. revert — 이미 공유된 역사를 다루는 유일한 방법
- 5. reflog — 어디까지, 언제까지 남는가
- 6. gc — 객체가 실제로 삭제되는 시점
- 7. fsck — reflog에도 없을 때 마지막 그물
- 8. 강제 푸시 사고와 팀 상황
- 퀴즈: 실력을 확인해 보세요
- 마치며
- 참고 자료
- 이어서 읽기
들어가며
Git에서 무언가를 잘못했을 때 가장 먼저 알아야 할 것은 명령이 아닙니다. 지금 잃어버린 것이 아직 어딘가에 남아 있는지입니다. 남아 있다면 침착하게 꺼내면 되고, 남아 있지 않다면 다른 사람의 사본을 찾아야 합니다. 이 판단을 못 하면 복구 가능한 상황에서 시간을 낭비하고, 복구 불가능한 상황에서 헛된 시도를 반복합니다.
이 블로그에는 git 되돌리기 — 상황별로 고르는 restore, reset, revert, reflog가 이미 있습니다. 그 글은 혼자 작업하는 로컬 흐름을 상황별로 안내하는 실전 치트시트입니다. 이 글은 그 옆에 놓을 레퍼런스를 목표로 합니다. 복구 가능성의 경계를 만료 정책과 함께 수치로 정리하고, 강제 푸시 사고나 워크트리·서브모듈처럼 팀 환경에서만 생기는 상황까지 다룹니다. 치트시트가 "지금 뭘 칠까"에 답한다면, 이 글은 "이건 되살릴 수 있는가"에 답합니다.
기준 버전은 Git 2.40 이상입니다. git restore와 git switch는 2.23에서 도입되어 오랫동안 실험적 표시가 붙어 있었으므로, 오래된 환경에서는 git checkout을 써야 할 수 있습니다.
1. 복구 가능성 지도 — 이 글의 핵심 한 장
먼저 결론입니다. 아래 표가 이 글에서 가장 중요합니다.
| 잃어버린 것 | 복구 가능성 | 수단 |
|---|---|---|
| 커밋된 내용 | 매우 높음 | reflog, fsck |
스테이징(git add)했으나 미커밋 | 가능 | git fsck --lost-found의 blob |
| 수정만 하고 add도 커밋도 안 한 변경 | 불가능 | 에디터 로컬 히스토리, 백업만 |
| stash 했다가 drop한 것 | 가능(만료 전) | reflog, fsck |
| 삭제한 브랜치 | 매우 높음 | reflog, fsck |
| rebase로 사라진 커밋 | 매우 높음 | ORIG_HEAD, reflog |
| 강제 푸시로 원격에서 사라진 커밋 | 조건부 | 누군가의 로컬 사본, 서버 reflog |
gc --prune=now 이후의 도달 불가 객체 | 불가능 | 없음 |
추적되지 않는 파일을 git clean | 불가능 | 없음 |
굵게 표시한 세 줄이 경계선입니다. Git이 지켜 주는 범위는 "한 번이라도 객체 데이터베이스에 기록된 것"입니다. 기록된 적 없는 변경은 Git의 책임 밖입니다.
그래서 실무 규칙 하나가 나옵니다. 위험한 작업을 하기 전에 일단 커밋하거나 stash 하세요. 임시 커밋 하나가 복구 가능성을 "불가능"에서 "매우 높음"으로 바꿉니다.
2. 세 개의 영역과 그것을 옮기는 명령
Git은 세 곳에 상태를 둡니다. 작업 트리, 인덱스(스테이징 영역), 그리고 HEAD가 가리키는 커밋입니다. 되돌리기 명령은 결국 "어느 영역을 어디에 맞출 것인가"입니다.
git status
git status --short --branch
git diff
git diff --staged
git diff는 작업 트리와 인덱스의 차이입니다.git diff --staged는 인덱스와 HEAD의 차이입니다.
이 두 명령의 차이를 명확히 알면 git status의 안내문이 정확히 무슨 뜻인지 이해됩니다.
작업 트리와 인덱스를 되돌리는 현대적 명령은 git restore입니다.
git restore path/to/file
git restore --staged path/to/file
git restore --staged --worktree path/to/file
git restore --source=HEAD~2 path/to/file
- 인자 없이
git restore <파일>은 작업 트리의 수정을 버립니다. 커밋도 add도 하지 않은 변경이라면 이 시점에 영원히 사라집니다. --staged는 인덱스만 되돌립니다. 작업 트리 수정은 남습니다.--source는 특정 커밋의 내용으로 가져옵니다.
파괴적 명령 경고: git restore <파일>은 확인을 묻지 않고 되돌립니다. 되살릴 방법이 없으므로, 버리려는 내용이 정말 필요 없는지 git diff로 먼저 확인하세요.
3. reset — 세 가지 모드의 실제 차이
git reset은 HEAD를 옮깁니다. 옵션은 인덱스와 작업 트리를 어디까지 함께 옮길지를 정합니다.
git reset --soft HEAD~1
git reset --mixed HEAD~1
git reset --hard HEAD~1
| 모드 | HEAD | 인덱스 | 작업 트리 | 언제 쓰나 |
|---|---|---|---|---|
--soft | 이동 | 유지 | 유지 | 커밋만 취소하고 다시 커밋할 때 |
--mixed | 이동 | 이동 | 유지 | 기본값. add까지 취소할 때 |
--hard | 이동 | 이동 | 이동 | 전부 버릴 때 |
파괴적 명령 경고: git reset --hard는 작업 트리의 미커밋 변경을 삭제하며, 그 변경은 복구할 수 없습니다. 커밋 자체는 reflog로 되살릴 수 있지만, 커밋된 적 없는 수정은 아닙니다. 이 구분이 매우 중요합니다.
안전한 습관은 실행 전에 반드시 확인하는 것입니다.
git status
git stash push -m 'before hard reset 2026-08-15'
git reset --hard HEAD~1
git reset --hard 직후 "아, 그 커밋이 필요했다"면 reflog가 답입니다.
git reflog
git reset --hard HEAD@{1}
ORIG_HEAD도 유용합니다. reset, merge, rebase 같은 명령은 실행 직전의 HEAD를 여기에 저장합니다.
git reset --hard ORIG_HEAD
4. revert — 이미 공유된 역사를 다루는 유일한 방법
푸시된 커밋을 없애려고 reset 후 강제 푸시를 하면, 그 브랜치를 이미 받아 간 모든 사람의 저장소가 어긋납니다. 공유된 역사에는 revert를 씁니다. 역사를 지우는 대신 반대 내용을 담은 새 커밋을 추가하는 방식입니다.
git revert 3f2a1b9
git revert --no-commit 3f2a1b9
git revert HEAD~3..HEAD
git revert --abort
머지 커밋을 되돌릴 때는 어느 쪽을 남길지 지정해야 합니다.
git revert -m 1 8c4d2e0
-m 1은 첫 번째 부모(대개 머지를 받은 쪽, 즉 main)를 기준으로 삼겠다는 뜻입니다. 부모 번호는 다음으로 확인합니다.
git log --merges -1 --format='%H %P'
머지 revert에는 후속 함정이 있습니다. 머지를 되돌린 뒤 같은 브랜치를 다시 머지하면, Git은 이미 머지된 것으로 판단해 되돌린 변경을 다시 가져오지 않습니다. 이 경우 "revert의 revert"를 하거나 브랜치를 다시 만들어야 합니다. 팀 작업에서 흔히 겪는 혼란이므로, 머지 revert를 할 때는 그 브랜치를 나중에 어떻게 다시 넣을지 계획을 함께 세우세요.
5. reflog — 어디까지, 언제까지 남는가
reflog는 참조가 움직인 기록입니다. 커밋 해시를 몰라도 "두 단계 전 HEAD"를 지칭할 수 있게 해 줍니다.
git reflog
git reflog show main
git reflog --date=iso | head -20
git log -g --oneline
여기서 반드시 알아야 할 세 가지 성질이 있습니다.
첫째, reflog는 로컬입니다. 문서가 명시하듯 reflog는 저장소별로 존재하며 푸시되지 않습니다. 다른 사람의 저장소에는 여러분의 reflog가 없고, 여러분이 새로 clone하면 reflog는 비어 있습니다. clone 직후에는 안전망이 없다는 뜻입니다.
둘째, 만료 기한이 있습니다. 기본값은 명확합니다.
| 설정 | 기본값 | 대상 |
|---|---|---|
gc.reflogExpire | 90일 | 현재 팁에서 도달 가능한 항목 |
gc.reflogExpireUnreachable | 30일 | 도달 불가능한 항목 |
즉 rebase나 reset으로 버려진 커밋의 reflog 항목은 30일이 지나면 정리 대상이 됩니다. 중요한 저장소라면 늘릴 수 있습니다.
git config gc.reflogExpire '365 days'
git config gc.reflogExpireUnreachable '180 days'
git config --get gc.reflogExpire
셋째, reflog 항목이 남아 있어도 객체가 지워졌으면 소용없습니다. 다음 절에서 이어집니다.
브랜치를 실수로 지웠을 때의 복구는 이렇습니다.
git reflog --all | grep -i 'feature/payment'
git branch feature/payment 9a1c3f7
git reflog expire는 직접 쓸 일이 드물지만, 무엇을 지울지 미리 볼 수 있습니다.
git reflog expire --dry-run --expire-unreachable=30.days --all
--dry-run(-n)은 실제로 지우지 않고 대상만 보여 줍니다.
6. gc — 객체가 실제로 삭제되는 시점
Git은 도달 불가능한 객체를 즉시 지우지 않습니다. git gc가 정리하되 유예 기간을 둡니다.
| 설정 | 기본값 | 의미 |
|---|---|---|
gc.pruneExpire | 2.weeks.ago | 이보다 오래된 느슨한 객체를 정리 |
gc.auto | 6700 | 느슨한 객체가 이 정도를 넘으면 자동 실행 |
문서에 따르면 git gc는 기본적으로 2주보다 오래된 도달 불가 객체를 제거합니다. 유예 기간은 다른 프로세스가 동시에 쓰는 중일 때 손상을 막기 위한 장치이기도 합니다.
절대 하지 말아야 할 것이 있습니다.
# 위험: 복구 여지를 즉시 없앤다
git gc --prune=now
git reflog expire --expire-unreachable=now --all
파괴적 명령 경고: 이 두 명령을 함께 실행하면 도달 불가능한 객체가 즉시 삭제되어 어떤 방법으로도 복구할 수 없습니다. 저장소 용량을 줄이려고 검색해서 나온 명령을 그대로 붙여 넣는 사고가 흔합니다. 사고 직후 복구를 시도하는 중이라면 gc를 절대 실행하지 마세요. 문서 역시 --prune=now가 동시 쓰기 상황에서 손상 위험을 높인다고 경고합니다.
반대로, 복구 여지를 늘리려면 다음처럼 설정합니다.
git config gc.pruneExpire '90 days'
git config gc.auto 0
gc.auto 0은 자동 gc를 끕니다. 큰 저장소에서 예기치 않은 시점에 gc가 도는 것을 막고 싶을 때 씁니다. 대신 주기적으로 수동 실행이 필요합니다.
7. fsck — reflog에도 없을 때 마지막 그물
reflog 항목이 만료되었지만 객체는 아직 남아 있는 상태가 있습니다. 이때 git fsck로 고아 객체를 찾습니다.
git fsck --unreachable
git fsck --dangling
git fsck --lost-found
git fsck --no-reflogs --unreachable --name-objects
--unreachable은 어떤 참조에서도 도달할 수 없는 객체를 나열합니다.--dangling은 직접 참조되지 않는 객체를 보여 줍니다(기본 동작).--lost-found는 찾은 객체를 파일로 꺼내 줍니다. 문서에 따르면 커밋은.git/lost-found/commit/, 그 외는.git/lost-found/other/에 기록되며, blob은 이름 대신 내용이 파일로 저장됩니다.--no-reflogs는 reflog로만 참조되는 커밋을 도달 가능으로 치지 않습니다. "예전에 브랜치였지만 지금은 아닌" 커밋을 찾을 때 씁니다.--name-objects는 그 객체에 어떻게 도달할 수 있는지 경로를 함께 보여 줍니다.
찾은 커밋의 내용을 확인하고 살립니다.
git fsck --lost-found
ls .git/lost-found/commit/
git show --stat 4b7e91c
git branch recovered-4b7e91c 4b7e91c
커밋하지 않고 git add만 한 상태에서 reset --hard를 했다면 blob은 남아 있습니다. 파일 이름은 잃어버렸지만 내용은 살릴 수 있습니다.
git fsck --lost-found
grep -rl 'function calculateTax' .git/lost-found/other/ | head
내용의 특징적인 문자열로 찾는 것이 현실적인 방법입니다.
stash도 같은 원리로 되살립니다. git stash drop이나 git stash clear는 스택 항목만 지울 뿐 객체는 남기므로, 만료 전이라면 복구할 수 있습니다.
git fsck --unreachable | grep commit
git show --stat 7d3e5a2
git stash apply 7d3e5a2
stash는 내부적으로 커밋 객체로 저장되기 때문에 git stash apply에 해시를 직접 넘길 수 있습니다. 다만 여기서도 시간 제한은 같습니다. gc가 이미 정리했다면 어떤 명령으로도 되살릴 수 없습니다.
정리하면 fsck는 "객체는 남아 있는데 그것을 가리키는 이름이 사라진 상황" 을 위한 도구입니다. 객체 자체가 삭제된 뒤에는 아무 역할도 하지 못합니다. 그래서 사고를 인지한 직후에 해야 할 첫 번째 행동은 복구 명령을 찾는 것이 아니라, 그 저장소에서 gc가 돌지 않도록 막는 것입니다.
8. 강제 푸시 사고와 팀 상황
원격 브랜치가 강제 푸시로 덮였을 때 복구 경로는 세 갈래입니다.
경로 1 — 덮어쓴 사람의 로컬 reflog. 가장 빠릅니다.
git reflog show origin/main
git log --oneline origin/main@{1}
경로 2 — 다른 사람의 로컬 사본. 아직 fetch하지 않은 사람이 있다면 그 사람의 원격 추적 브랜치에 이전 상태가 남아 있습니다. 사고를 인지한 즉시 팀 채널에 "지금 fetch/pull 하지 마세요"라고 알리는 것이 실질적으로 가장 중요한 조치입니다.
경로 3 — 서버 쪽 기록. GitHub, GitLab 등 대부분의 호스팅은 이벤트 기록에 이전 커밋 해시를 남깁니다. 해시만 알면 되살릴 수 있습니다.
git fetch origin 9a1c3f7
git branch rescue 9a1c3f7
애초에 사고를 줄이는 설정도 있습니다.
git push --force-with-lease origin main
git push --force-with-lease=main:9a1c3f7 origin main
--force-with-lease는 원격이 내가 마지막으로 본 상태 그대로일 때만 덮어씁니다. 그 사이에 다른 사람이 푸시했다면 거부됩니다. 강제 푸시가 필요한 상황이라면 항상 이쪽을 쓰세요. 다만 fetch를 먼저 해 버리면 lease 기준이 갱신되어 보호 효과가 약해진다는 점은 알아 두어야 합니다.
워크트리와 서브모듈은 별도로 신경 써야 합니다.
git worktree list
git worktree remove ../feature-wt
git submodule status
git submodule update --init --recursive
워크트리를 강제 제거하면 그 안의 미커밋 변경은 사라집니다. 서브모듈은 부모 저장소가 특정 커밋을 가리킬 뿐이므로, 서브모듈 저장소에서 그 커밋이 사라지면 부모의 참조는 살아 있어도 내용을 가져올 수 없습니다. 서브모듈 쪽 브랜치를 지우기 전에 부모가 참조하는 커밋인지 확인하세요.
마지막으로, Git이 절대 지켜 주지 않는 것을 다시 강조합니다.
git clean -n -d
git clean -f -d
파괴적 명령 경고: git clean -f -d는 추적되지 않는 파일과 디렉터리를 삭제하며 복구 수단이 전혀 없습니다. 반드시 -n(dry run)으로 목록을 먼저 확인하세요. 설정 파일이나 로컬 데이터가 목록에 있으면 실행하지 말고 제외 패턴을 지정해야 합니다.
퀴즈: 실력을 확인해 보세요
퀴즈 1: git reset --hard로 되돌렸습니다. 어떤 것이 복구되고 어떤 것이 복구되지 않나요?
정답: 커밋된 내용은 reflog로 복구되지만, 커밋된 적 없는 작업 트리 수정은 복구되지 않습니다
설명: reset은 HEAD를 옮길 뿐 객체를 지우지 않으므로, 버려진 커밋은 여전히 객체 데이터베이스에 있습니다.
git reflog
git reset --hard HEAD@{1}
반면 편집만 하고 add도 커밋도 하지 않은 변경은 Git이 한 번도 본 적이 없습니다. 이 경우 남은 희망은 에디터의 로컬 히스토리나 파일시스템 스냅샷뿐입니다. 그래서 위험한 작업 전에는 임시 커밋이나 stash를 먼저 하는 것이 규칙입니다.
퀴즈 2: 3주 전 rebase에서 사라진 커밋을 찾으려 합니다. reflog에 없다면 다음 수단은?
정답: git fsck로 도달 불가 객체를 찾습니다. 단 gc가 이미 정리했다면 불가능합니다
설명: 도달 불가 reflog 항목의 기본 만료는 30일이므로 3주면 아직 남아 있을 가능성이 큽니다. 없다면 객체 자체를 찾습니다.
git fsck --lost-found --no-reflogs --unreachable
ls .git/lost-found/commit/
git show --stat <해시>
gc.pruneExpire의 기본값은 2주이므로, 그 사이에 gc가 실행되었다면 이미 삭제되었을 수 있습니다. 이것이 중요한 저장소에서 만료 설정을 늘려 두는 이유입니다.
퀴즈 3: 동료가 main에 강제 푸시를 했습니다. 가장 먼저 할 행동은?
정답: 팀에 fetch와 pull을 중단하라고 알리는 것입니다
설명: 기술적 복구보다 이 커뮤니케이션이 먼저입니다. 아직 fetch하지 않은 사람의 저장소에는 이전 상태의 origin/main이 남아 있으며, 그것이 가장 확실한 사본입니다. 모두가 fetch해 버리면 그 사본이 사라집니다.
이후 복구는 다음 순서입니다.
git reflog show origin/main
git log --oneline origin/main@{1}
git branch rescue origin/main@{1}
재발 방지는 브랜치 보호 규칙과 --force-with-lease 사용 규칙입니다.
퀴즈 4: 저장소가 크다는 이유로 검색 결과에서 본 gc 명령을 실행하려 합니다. 어떤 조합이 위험한가요?
정답: reflog를 즉시 만료시키고 gc --prune=now를 함께 실행하는 조합입니다
설명: 다음 조합은 도달 불가능한 모든 객체를 즉시 삭제하며 복구 수단을 남기지 않습니다.
git reflog expire --expire-unreachable=now --all
git gc --prune=now
특히 복구 작업 중에는 절대 실행하면 안 됩니다. 용량이 목적이라면 무엇이 용량을 차지하는지 먼저 확인하는 편이 낫습니다.
git count-objects -vH
대용량 파일이 히스토리에 들어간 것이 원인이라면 gc가 아니라 히스토리 재작성이 필요하며, 그것은 팀 전체가 합의해야 하는 작업입니다.
퀴즈 5: 머지 커밋을 revert한 뒤 같은 기능 브랜치를 다시 머지했는데 변경이 들어오지 않습니다. 왜일까요?
정답: Git은 그 브랜치를 이미 머지된 것으로 보기 때문입니다
설명: revert는 역사를 지우지 않고 반대 변경을 추가합니다. 머지 커밋 자체는 히스토리에 남아 있으므로, 다시 머지해도 "이미 합쳐진 커밋"으로 판단해 아무것도 가져오지 않습니다. 해결책은 두 가지입니다.
git revert <revert 커밋 해시>
되돌리기를 다시 되돌리거나, 기능 브랜치를 새 브랜치로 다시 만들어 커밋을 옮겨 담습니다.
git switch -c feature/payment-v2 origin/main
git cherry-pick <커밋 범위>
퀴즈 6: 방금 clone한 저장소에서 reset --hard를 실행했습니다. 안전망 측면에서 무엇이 다를까요?
정답: clone 직후에는 reflog에 이전 상태가 거의 없어 되돌릴 지점이 부족합니다
설명: reflog는 저장소별 로컬 기록이며 clone으로 전달되지 않습니다. 갓 clone한 저장소의 reflog에는 clone 항목 하나뿐입니다. 다만 원격 추적 브랜치가 그대로 있으므로 원격 상태로는 언제든 돌아갈 수 있습니다.
git reflog
git reset --hard origin/main
위험한 것은 clone 직후 로컬에서만 만든 작업입니다. 그 작업은 원격에도 없고 reflog 이력도 짧으므로, 중간중간 커밋해 두는 습관이 유일한 방어입니다.
마치며
Git 되돌리기에서 외워야 할 것은 명령 목록이 아니라 세 개의 시간입니다. 도달 불가 reflog 항목은 기본 30일, 도달 가능 항목은 90일, 느슨한 객체는 gc 기준 2주입니다. 이 숫자를 알면 "3주 전 것을 찾을 수 있을까"라는 질문에 근거를 가지고 답할 수 있습니다.
그리고 경계선 하나를 기억하세요. Git은 자신이 본 적 있는 것만 지켜 줍니다. 커밋되지 않은 수정, 추적되지 않는 파일, clean으로 지운 것은 Git의 보호 밖입니다.
실무 습관으로 바꾸면 이렇게 됩니다. 위험한 작업 전에는 임시 커밋을 만들고, 강제 푸시는 반드시 --force-with-lease로 하고, 중요한 저장소에서는 만료 설정을 늘려 둡니다. 이 세 가지면 대부분의 사고가 "복구 가능" 쪽으로 넘어옵니다.
참고 자료
- git-reflog 공식 문서 (2026-08-15 확인)
- git-gc 공식 문서 (2026-08-15 확인)
- git-fsck 공식 문서 (2026-08-15 확인)
- git-reset 공식 문서 (2026-08-15 확인)
- git-revert 공식 문서 (2026-08-15 확인)
이어서 읽기
- 이전 글: SSH 운영 완전 가이드
- 다음 글: 리눅스 로그 운영 완전 가이드
- git 되돌리기 — 상황별로 고르는 restore, reset, revert, reflog — 로컬 작업 흐름 중심의 치트시트
- Git 객체와 내부 구조 — 객체 데이터베이스가 무엇을 보관하는가
- Git 플레이그라운드 — 명령의 결과를 안전하게 실험
- Git 명령어 찾기 — 상황에 맞는 명령 검색
Undoing Things in Git, the Complete Guide: What Comes Back and What Is Gone Forever
- Introduction
- 1. The recoverability map — the one table that matters
- 2. The three areas and the commands that move between them
- 3. reset — what actually differs between the three modes
- 4. revert — the only way to handle history that is already shared
- 5. reflog — how far back, and for how long
- 6. gc — the moment objects are actually deleted
- 7. fsck — the last net when it is not even in the reflog
- 8. Force-push accidents and team situations
- Quiz: check your understanding
- Closing
- References
- Further reading
Introduction
When something goes wrong in Git, the first thing you need to know is not a command. It is whether the thing you just lost still exists somewhere. If it does, you calmly retrieve it; if it does not, you go looking for someone else's copy. Without that judgement you waste time in situations that were recoverable, and you keep making futile attempts in situations that were not.
This blog already has Undoing things in git — choosing restore, reset, revert, and reflog by situation. That article is a hands-on cheat sheet that walks you through local, solo workflows situation by situation. This one aims to be the reference you keep next to it. It puts numbers on the boundary of recoverability along with the expiry policies, and covers the situations that only arise in a team environment, such as force-push accidents, worktrees, and submodules. If the cheat sheet answers "what do I type right now", this article answers "can this be brought back at all".
The baseline version is Git 2.40 or newer. git restore and git switch were introduced in 2.23 and carried an experimental marker for a long time, so on older environments you may still need git checkout.
1. The recoverability map — the one table that matters
The conclusion comes first. The table below is the most important thing in this article.
| What you lost | Recoverability | Means |
|---|---|---|
| Committed content | Very high | reflog, fsck |
Staged with git add but never committed | Possible | the blob from git fsck --lost-found |
| Edited but never added and never committed | Impossible | Editor local history or a backup only |
| Stashed and then dropped | Possible (before expiry) | reflog, fsck |
| A deleted branch | Very high | reflog, fsck |
| A commit lost to rebase | Very high | ORIG_HEAD, reflog |
| A commit erased from the remote by force push | Conditional | Someone's local copy, server-side reflog |
Unreachable objects after gc --prune=now | Impossible | None |
Untracked files removed by git clean | Impossible | None |
The three rows in bold are the boundary. What Git protects for you is "anything that was written into the object database at least once". A change it never recorded is outside Git's responsibility.
That gives you one practical rule. Before doing anything dangerous, commit or stash first. A single throwaway commit moves your recoverability from "impossible" to "very high".
2. The three areas and the commands that move between them
Git keeps state in three places: the working tree, the index (the staging area), and the commit that HEAD points at. Undo commands ultimately answer the question "which area am I bringing in line with what".
git status
git status --short --branch
git diff
git diff --staged
git diffis the difference between the working tree and the index.git diff --stagedis the difference between the index and HEAD.
Once the distinction between these two is clear, the guidance text git status prints starts to mean exactly what it says.
The modern command for reverting the working tree and the index is git restore.
git restore path/to/file
git restore --staged path/to/file
git restore --staged --worktree path/to/file
git restore --source=HEAD~2 path/to/file
- With no options,
git restore <file>throws away the modifications in the working tree. If the change was never committed and never added, it disappears forever at that moment. --stagedreverts the index only. Working tree modifications remain.--sourcepulls the content from a specific commit.
Destructive command warning: git restore <file> reverts without asking for confirmation. There is no way to bring it back, so check with git diff first that you really do not need what you are about to discard.
3. reset — what actually differs between the three modes
git reset moves HEAD. The option decides how far the index and the working tree come along with it.
git reset --soft HEAD~1
git reset --mixed HEAD~1
git reset --hard HEAD~1
| Mode | HEAD | Index | Working tree | When to use it |
|---|---|---|---|---|
--soft | Moves | Stays | Stays | Undo just the commit and recommit |
--mixed | Moves | Moves | Stays | The default. Undo the add as well |
--hard | Moves | Moves | Moves | Throw everything away |
Destructive command warning: git reset --hard deletes uncommitted changes in the working tree, and those changes cannot be recovered. The commits themselves can be brought back with reflog, but modifications that were never committed cannot. This distinction matters enormously.
The safe habit is to always check before running it.
git status
git stash push -m 'before hard reset 2026-08-15'
git reset --hard HEAD~1
If right after git reset --hard you think "wait, I needed that commit", reflog is the answer.
git reflog
git reset --hard HEAD@{1}
ORIG_HEAD is useful too. Commands such as reset, merge, and rebase save the HEAD from immediately before they ran into it.
git reset --hard ORIG_HEAD
4. revert — the only way to handle history that is already shared
If you try to erase a pushed commit with reset followed by a force push, every repository that already pulled that branch goes out of sync. For shared history you use revert. Instead of erasing history, it adds a new commit containing the opposite change.
git revert 3f2a1b9
git revert --no-commit 3f2a1b9
git revert HEAD~3..HEAD
git revert --abort
When reverting a merge commit you have to say which side to keep.
git revert -m 1 8c4d2e0
-m 1 means "take the first parent as the baseline", which is usually the side that received the merge, that is, main. You can check the parent numbers like this.
git log --merges -1 --format='%H %P'
Reverting a merge has a follow-up trap. After you revert a merge, if you merge the same branch again Git considers it already merged and does not bring the reverted changes back. In that case you either "revert the revert" or recreate the branch. This is a common source of confusion in team work, so when you revert a merge, plan up front how that branch is going to get back in later.
5. reflog — how far back, and for how long
The reflog is a record of where references have moved. It lets you refer to "HEAD two steps ago" without knowing the commit hash.
git reflog
git reflog show main
git reflog --date=iso | head -20
git log -g --oneline
There are three properties here you absolutely have to know.
First, the reflog is local. As the documentation states, the reflog exists per repository and is not pushed. Your reflog is not in anyone else's repository, and if you make a fresh clone the reflog is empty. That means there is no safety net right after a clone.
Second, entries expire. The defaults are explicit.
| Setting | Default | Applies to |
|---|---|---|
gc.reflogExpire | 90 days | Entries reachable from the current tip |
gc.reflogExpireUnreachable | 30 days | Unreachable entries |
In other words, the reflog entries for commits abandoned by a rebase or a reset become eligible for cleanup after 30 days. For an important repository you can raise this.
git config gc.reflogExpire '365 days'
git config gc.reflogExpireUnreachable '180 days'
git config --get gc.reflogExpire
Third, a surviving reflog entry is useless if the object has already been deleted. That leads into the next section.
Recovering a branch you deleted by accident looks like this.
git reflog --all | grep -i 'feature/payment'
git branch feature/payment 9a1c3f7
You rarely reach for git reflog expire directly, but it can show you in advance what would be removed.
git reflog expire --dry-run --expire-unreachable=30.days --all
--dry-run (-n) shows the targets without actually deleting anything.
6. gc — the moment objects are actually deleted
Git does not delete unreachable objects immediately. git gc cleans them up, but with a grace period.
| Setting | Default | Meaning |
|---|---|---|
gc.pruneExpire | 2.weeks.ago | Prune loose objects older than this |
gc.auto | 6700 | Run automatically once loose objects exceed roughly this many |
According to the documentation, git gc by default removes unreachable objects older than two weeks. The grace period also exists to prevent corruption when another process is writing at the same time.
There is something you must never do.
# Dangerous: removes every chance of recovery instantly
git gc --prune=now
git reflog expire --expire-unreachable=now --all
Destructive command warning: running these two together deletes unreachable objects immediately, and there is no method whatsoever to recover them. A common accident is pasting in a command someone found while searching for how to shrink a repository. If you are in the middle of trying to recover from an incident, never run gc. The documentation likewise warns that --prune=now increases the risk of corruption when there are concurrent writes.
Conversely, to widen your recovery window, configure it like this.
git config gc.pruneExpire '90 days'
git config gc.auto 0
gc.auto 0 turns automatic gc off. You use it when you want to stop gc from running at an unexpected moment in a large repository. In exchange you have to run it manually on a regular schedule.
7. fsck — the last net when it is not even in the reflog
There is a state in which the reflog entry has expired but the object is still there. That is when you use git fsck to find orphaned objects.
git fsck --unreachable
git fsck --dangling
git fsck --lost-found
git fsck --no-reflogs --unreachable --name-objects
--unreachablelists objects that cannot be reached from any reference.--danglingshows objects that are not directly referenced (the default behaviour).--lost-foundwrites the objects it finds out as files. According to the documentation, commits go into.git/lost-found/commit/and everything else into.git/lost-found/other/, and blobs are stored as files whose content is the blob, since the name is unknown.--no-reflogsstops treating commits referenced only by the reflog as reachable. You use it to find commits that "used to be a branch but are not any more".--name-objectsalso shows the path by which the object can be reached.
Check the content of the commit you found and rescue it.
git fsck --lost-found
ls .git/lost-found/commit/
git show --stat 4b7e91c
git branch recovered-4b7e91c 4b7e91c
If you ran reset --hard while the work was only git add-ed and never committed, the blob is still there. You lost the file name, but the content can be rescued.
git fsck --lost-found
grep -rl 'function calculateTax' .git/lost-found/other/ | head
Searching by a distinctive string from the content is the realistic approach.
A stash is rescued on the same principle. git stash drop and git stash clear only remove the stack entry and leave the objects behind, so as long as they have not expired you can recover them.
git fsck --unreachable | grep commit
git show --stat 7d3e5a2
git stash apply 7d3e5a2
Because a stash is stored internally as a commit object, you can hand a hash directly to git stash apply. The same time limit applies here as well, though: if gc has already cleaned up, no command will bring it back.
To summarise, fsck is a tool for the situation where "the object is still there but the name pointing at it is gone". Once the object itself has been deleted it can do nothing at all. Which is why the first thing to do the instant you notice an accident is not to go looking for a recovery command, but to stop gc from running in that repository.
8. Force-push accidents and team situations
When a remote branch has been overwritten by a force push, there are three recovery paths.
Path 1 — the local reflog of whoever overwrote it. This is the fastest.
git reflog show origin/main
git log --oneline origin/main@{1}
Path 2 — someone else's local copy. If anybody has not fetched yet, the previous state is still in their remote-tracking branch. The single most valuable action the moment you notice the accident is telling the team channel not to fetch or pull right now.
Path 3 — the server-side record. GitHub, GitLab, and most other hosts keep the previous commit hash in their event log. If you have the hash, you can bring it back.
git fetch origin 9a1c3f7
git branch rescue 9a1c3f7
There are also settings that reduce the chance of the accident in the first place.
git push --force-with-lease origin main
git push --force-with-lease=main:9a1c3f7 origin main
--force-with-lease overwrites only if the remote is exactly in the state you last saw. If someone else pushed in the meantime, it is refused. Whenever you need a force push, always use this form. Be aware, though, that fetching first refreshes the lease baseline and weakens the protection.
Worktrees and submodules need separate attention.
git worktree list
git worktree remove ../feature-wt
git submodule status
git submodule update --init --recursive
Force-removing a worktree destroys the uncommitted changes inside it. A submodule is only a pointer from the parent repository to a specific commit, so if that commit disappears in the submodule repository, the parent's reference survives but the content cannot be fetched. Before deleting a branch on the submodule side, check whether it is a commit the parent references.
Finally, a reminder of what Git never protects for you.
git clean -n -d
git clean -f -d
Destructive command warning: git clean -f -d deletes untracked files and directories and there is no means of recovery at all. Always check the list first with -n (dry run). If config files or local data show up in the list, do not run it — specify exclusion patterns instead.
Quiz: check your understanding
Quiz 1: You undid work with git reset --hard. What is recovered and what is not?
Answer: Committed content is recovered through the reflog, but working tree modifications that were never committed are not
Why: reset only moves HEAD; it does not delete objects, so the abandoned commits are still in the object database.
git reflog
git reset --hard HEAD@{1}
Changes that were only edited, never added and never committed, on the other hand, are something Git has never seen. In that case your remaining hope is your editor's local history or a filesystem snapshot. That is exactly why the rule is to make a throwaway commit or a stash before doing anything dangerous.
Quiz 2: You want to find a commit that disappeared in a rebase three weeks ago. If it is not in the reflog, what is your next move?
Answer: Find the unreachable object with git fsck. Unless gc has already cleaned it up, in which case it is impossible
Why: The default expiry for unreachable reflog entries is 30 days, so at three weeks there is a good chance it is still there. If it is not, go looking for the object itself.
git fsck --lost-found --no-reflogs --unreachable
ls .git/lost-found/commit/
git show --stat <hash>
The default for gc.pruneExpire is two weeks, so if gc ran during that window it may already be deleted. This is why you raise the expiry settings on repositories that matter.
Quiz 3: A colleague force-pushed to main. What is the very first thing you do?
Answer: Tell the team to stop fetching and pulling
Why: This communication comes before any technical recovery. Anyone who has not fetched yet still has the previous state in their origin/main, and that is the most reliable copy there is. Once everybody fetches, that copy is gone.
Recovery afterwards goes in this order.
git reflog show origin/main
git log --oneline origin/main@{1}
git branch rescue origin/main@{1}
Preventing a recurrence is a matter of branch protection rules and a rule about using --force-with-lease.
Quiz 4: The repository is large, so you are about to run a gc command you found in search results. Which combination is dangerous?
Answer: Expiring the reflog immediately and running gc --prune=now together
Why: The following combination deletes every unreachable object immediately and leaves no means of recovery.
git reflog expire --expire-unreachable=now --all
git gc --prune=now
It must never be run while recovery work is in progress in particular. If size is the goal, it is better to first find out what is taking up the space.
git count-objects -vH
If the cause is a large file that made it into history, the fix is not gc but a history rewrite, and that is work the whole team has to agree on.
Quiz 5: You reverted a merge commit, then merged the same feature branch again, and the changes did not come in. Why?
Answer: Because Git considers that branch already merged
Why: revert does not erase history; it adds the opposite change. The merge commit itself is still in the history, so on a second merge Git treats those as "commits already merged" and brings nothing in. There are two ways out.
git revert <hash of the revert commit>
Either revert the revert, or recreate the feature branch as a new branch and move the commits over.
git switch -c feature/payment-v2 origin/main
git cherry-pick <commit range>
Quiz 6: You ran reset --hard in a repository you just cloned. What is different in terms of the safety net?
Answer: Right after a clone the reflog holds almost no previous state, so there are few points to go back to
Why: The reflog is a per-repository local record and is not transferred by clone. A freshly cloned repository has exactly one reflog entry, for the clone. The remote-tracking branches are intact, though, so you can always return to the remote state.
git reflog
git reset --hard origin/main
What is at risk is work created only locally right after the clone. That work is not on the remote either, and the reflog history is short, so committing as you go is the only defence.
Closing
What you need to memorise about undoing things in Git is not a list of commands but three durations. Unreachable reflog entries last 30 days by default, reachable entries 90 days, and loose objects two weeks by the gc default. Knowing these numbers lets you answer "can I find something from three weeks ago" with actual grounds.
And remember one boundary. Git only protects what it has seen. Uncommitted modifications, untracked files, and anything removed by clean are outside Git's protection.
Turned into working habits, it comes to this: make a throwaway commit before anything dangerous, always force-push with --force-with-lease, and raise the expiry settings on repositories that matter. Those three move most accidents over to the "recoverable" side.
References
- git-reflog official documentation (verified 2026-08-15)
- git-gc official documentation (verified 2026-08-15)
- git-fsck official documentation (verified 2026-08-15)
- git-reset official documentation (verified 2026-08-15)
- git-revert official documentation (verified 2026-08-15)
Further reading
- Previous: The complete guide to running SSH
- Next: The complete guide to Linux logging in production
- Undoing things in git — choosing restore, reset, revert, and reflog by situation — a cheat sheet centred on the local workflow
- Git objects and internals — what the object database actually keeps
- Git playground — experiment with command results safely
- Git command finder — search for the command that fits the situation