필사 모드: Undoing Things in Git, the Complete Guide: What Comes Back and What Is Gone Forever
English- 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
현재 단락 (1/197)
When something goes wrong in Git, the first thing you need to know is not a command. It is **whether...