TT Lab
Get started
Learn Learning paths Courses

Git in Practice

Undo Is Decided by Two Questions

Continue in TT Lab

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.