Undo Is Decided by Two Questions
One-line summary
How to undo is decided by two questions. Where is the thing you want to undo (working tree / index / commit), and has that commit already gone to other people?
Why this is needed
The reason Git's undo commands feel hard is not that there are many commands, but that nobody taught you the criteria for choosing one. The first question decides the kind of command, and the second question decides whether it is acceptable to alter the history.
Let us add one big reassurance. Something that has ever been committed can almost always be recovered, and something that has never been committed can almost never be recovered. So the dangerous moment is not "when you delete a commit" but "when you overwrite uncommitted changes".
How it works
The three modes of reset differ in the range they undo.
| Mode | Range undone | What you lose | When to use |
|---|---|---|---|
--soft |
Only the branch reference moves | Nothing | When regrouping several commits into one |
--mixed (default) |
Up to the index | The staging state | When you want to choose the staging again |
--hard |
Up to the index and the working tree | All uncommitted changes | When throwing away a local experiment wholesale |
--keep |
Up to the index, but aborts if there are overlapping local changes | Nothing | When you want to be refused before overwriting |
Rate the danger not by the command name but by "does this command overwrite uncommitted changes?".
If a commit is already in someone else's repository, every way of eliminating it breaks that person's history. Instead of altering the history, appending to the history is the answer. That is why you use revert. If you delete it with a force push, the local copies of those who already pulled it are unchanged, and when they push next, the deleted commit comes back to life.
There is one trap in reverting a merge. If you revert a merge, then fix the branch and merge it again, the reverted changes do not come back. This is because Git judges a merge only by the reachability of the history. The right answer is to revert the revert.
What it looks like in the field
The scene where someone's face goes pale after wiping out two commits with git reset --hard is common. Usually it is fine. Open git reflog and go back to HEAD@{1}. But you need to know two things. When you delete a branch, that branch's reference log is deleted with it (it remains in HEAD's reflog). And the default expiry is 90 days for reachable entries and 30 days for unreachable ones.
There are also commands that remove the safety net immediately. They are git reflog expire --expire=now --all and git gc --prune=now. A team that calls these two "cleanup" and runs them out of habit has no recovery means when an accident happens.
The real last resort is to find floating objects with git fsck --full --unreachable --no-reflogs and extract them with git cat-file -p <blob> > 파일. Content that was only staged is already an object, so it can be revived by this method.
What to use in which situation
If you attach the two earlier questions to real situations, you get a table like this. When you are in a hurry, with this table alone you do not have to agonize over commands.
| Situation | What to use | Caution |
|---|---|---|
| You just wrote the commit message wrong | commit --amend |
Do not do it if you have already pushed |
| You just left one file out of the commit | After adding, commit --amend --no-edit |
Same as above |
| You want to bundle three commits into one | reset --soft HEAD~3, then commit again |
The changes remain |
| You want to undo only the staging | restore --staged <파일> |
Does not touch the file contents |
| Put one file back to the last commit state | restore <파일> |
Uncommitted changes disappear |
| Cancel a commit you already pushed | revert <커밋> |
A new commit is created and the history stays |
| Throw away a local experiment wholesale | reset --hard |
The only place where there is no undo |
| You deleted something by mistake | Find it with reflog and reset --hard <해시> |
Within 30–90 days it usually comes back |
There are only two dangerous lines in the table. They are restore <파일> and reset --hard. Both overwrite changes that have never been committed, and those changes are recorded nowhere, so not even the reflog can revive them. So we recommend one habit. If you are unsure whether to throw something away, first commit it or push it aside with git stash. Both create objects, so from that moment there is a way to get it back.
One more thing about stash. By default it does not push aside new untracked files, so newly created files remain as they are and get mixed into the next task. To clear those away too, you must add -u, and if you do not know this you wander for a long time with "I stashed, so why is the working tree not clean?".
What you will do in the next lab
Since you need to create the same repository several times, first you make a script that stamps out repositories. Then you run amend, the three reset modes, revert and reflog recovery, each in a separate repository, and compare the differences in results with your own eyes, and finally you assemble a cheat sheet by situation.