Git & GitHub
Undoing Things Safely: Restore, Reset, Revert, Reflog and Friends
The undo tools run for real, recovering a lost commit with reflog, bisect, and a table of which command to use.
On this page
- Words used in this lesson
- Start here: what do I want to undo?
- Restore: undo edits to files
- Amend: fix the last commit
- Reset: move the branch
- Reflog: recover a lost commit
- Revert: undo a commit on shared history
- Stash: park unfinished work
- Cherry-pick: copy one commit
- Bisect: find the commit that broke it
- A safety checklist
- Try it
- Interview questions
- Key takeaways
- What's next
In one line: almost everything in Git can be undone, because Git keeps old commits around. The skill is choosing the right tool: restore for files, amend for the last commit, reset for local history, revert for shared history, and the reflog when you think you lost something.
Beginners fear Git because one wrong command seems to destroy work. In practice, once something is committed, it is very hard to lose it for good. What you can lose easily is work that was never committed. This lesson shows the undo tools one by one, each with a real run, and ends with a table you can keep next to your keyboard.
It builds on lesson 0 (commits and pointers) and lesson 3 (remotes). The idea that matters most: local history that nobody has seen can be rewritten. History that is already shared should only get new commits on top.
What I ran / checked. Everything ran with git version 2.43.0 on Linux in throwaway repositories (no GitHub). Configuration was ignored for repeatable output, and I used a fixed identity and date, so all commits show the same date. Your hashes will differ. git bisect ran against a tiny shell script I wrote. Commands that normally open an editor were given a message with -m, or the editor was set to the no-op command true. I read the git reset and git bisect manual sources and config/gc.adoc (git/git, Documentation/) to check the options, the bisect exit codes and the reflog expiry defaults, but I did not read the whole text of each.
Words used in this lesson
- To restore is to put file content back from the staging area or from a commit.
- To amend is to replace the last commit with a new, corrected one.
- To reset is to move the current branch to another commit (and optionally change the staging area and files).
- To revert is to make a new commit that undoes an older commit.
- The reflog is Git's private diary of where
HEADand each branch pointed. Entries expire after some time (the defaults inDocumentation/config/gc.adocare 90 days for reachable entries and 30 days for unreachable ones), so it is a short-term safety net. - A stash is a shelf where Git keeps unfinished changes so you can switch tasks.
- To cherry-pick is to copy one commit onto the current branch as a new commit.
- Bisect is a binary search through history to find the commit that introduced a bug.
Start here: what do I want to undo?
| I want to... | Command | Safe on shared history? |
|---|---|---|
| Throw away edits in a file (not committed) | git restore file | n/a (local only) |
| Unstage a file but keep the edits | git restore --staged file | n/a (local only) |
| Get an old version of one file | git restore --source=<commit> file | n/a |
| Fix the last commit message or add a forgotten file | git commit --amend | No, if already pushed |
| Un-commit but keep the changes staged | git reset --soft HEAD~1 | No, if already pushed |
| Un-commit and keep the changes unstaged | git reset HEAD~1 (mixed) | No, if already pushed |
| Un-commit and throw the changes away | git reset --hard HEAD~1 | No, and dangerous |
| Undo a commit that others already have | git revert <commit> | Yes |
| Recover a "lost" commit or deleted branch | git reflog, then git branch name <hash> | n/a |
| Park unfinished work for later | git stash | n/a (local only) |
| Copy one commit to another branch | git cherry-pick <commit> | Yes (makes a new commit) |
| Find which commit broke something | git bisect | n/a (read-only search) |
Restore: undo edits to files
git restore puts file content back. It replaced the old, confusing uses of git checkout -- file and git reset HEAD file.
echo "oops" >> app.txtgit status --shortgit restore app.txtcat app.txtgit status --short M app.txtv1The edit is gone, and status is clean. This cannot be undone. Git never saw those edits as a commit, so there is nothing to bring back. Run git diff first when you are not sure.
To unstage a file (the edits stay on disk):
echo "staged mistake" > secret.txtgit add secret.txtgit status --shortgit restore --staged secret.txtgit status --shortA secret.txt?? secret.txtTo get an old version of a file from another commit, use --source. I restored app.txt from the commit before the last one; it contained v1 again and status was clean. (In that run the old and the current versions were the same, so use a file that really changed when you try it.) Use git restore --source=HEAD~2 --staged --worktree file to restore both the staging area and the file.
Amend: fix the last commit
I made a commit with a typo in the message:
git commit -m "Add b.txt wrng"git commit --amend -m "Add b.txt"I also forgot a file. Stage it and amend with --no-edit (keep the message):
echo "bb" > bb.txt && git add bb.txtgit commit --amend --no-editgit log --oneline -3git show --stat --format=%s HEAD49924a8 Add b.txt1a01d5c Add a.txt3d03bdf Add app v1Add b.txt b.txt | 1 + bb.txt | 1 + 2 files changed, 2 insertions(+)The hash changed each time: the message Add b.txt wrng was commit 9f01fe8, the first amend made 16632af and the last 49924a8. As lesson 0 explained, an amended commit is a new commit. The old ones are still in the object store (the reflog lists both amends). That is fine on a local commit. It is a problem if you already pushed, because your teammates have the old one.
Reset: move the branch
git reset <commit> moves the current branch to <commit>. The three modes differ in what happens to the staging area and the working tree:
| Mode | Branch moves | Staging area | Working files |
|---|---|---|---|
--soft | Yes | Unchanged (your changes stay staged) | Unchanged |
--mixed (default) | Yes | Reset to the new commit | Unchanged (changes become unstaged) |
--hard | Yes | Reset | Reset: changes are lost |
Three commits f1, f2, f3 (newest c2df1d0 Add f3, then 6bd2f39 Add f2) sit on top of the earlier ones.
Soft: un-commit f3, keep it staged. Good for "I want to redo this commit".
git reset --soft HEAD~1git status --shortgit log --oneline | head -2A f3.txt6bd2f39 Add f2892a13b Add f1Mixed (the default): I recommitted, then un-committed again without any flag. The file stays on disk but is no longer staged. Good for "I want to split this into smaller commits".
git commit -qm "Add f3 again"git reset HEAD~1git status --short?? f3.txtHard: I committed again, then ran:
git reset --hard HEAD~1git status --shortlsHEAD is now at 6bd2f39 Add f2a.txtapp.txtb.txtbb.txtf1.txtf2.txtf3.txt is gone from the disk, too. If f3 had never been committed, those edits would be lost forever. Because it was committed, we can get it back with the reflog.
git reset --hard and git clean -fd are the two commands that can destroy uncommitted work. Run git status and git diff first. If you are not sure, git stash is a safe copy.
Reflog: recover a lost commit
The reflog records every move of HEAD. I made a clean example: three commits, then a hard reset by two.
git log --onelinegit reset --hard HEAD~2git log --onelinels9208aaa Add n34ff95ea Add n2e05f10d Add n1HEAD is now at e05f10d Add n1e05f10d Add n1n1.txtn2 and n3 seem gone. Look at the diary:
git refloge05f10d HEAD@{0}: reset: moving to HEAD~29208aaa HEAD@{1}: commit: Add n34ff95ea HEAD@{2}: commit: Add n2e05f10d HEAD@{3}: commit (initial): Add n1The commits are right there. HEAD@{1} is "where HEAD was one move ago". Go back:
git reset --hard 'HEAD@{1}'git log --onelinelsHEAD is now at 9208aaa Add n39208aaa Add n34ff95ea Add n2e05f10d Add n1n1.txtn2.txtn3.txtEverything is back. The same trick recovers a deleted branch. I made a branch risky with one commit, switched away and deleted it with -D:
git branch -D riskygit reflog | head -3git branch rescued 'HEAD@{1}'git log --oneline rescued | head -2Deleted branch risky (was b78fe8f).9208aaa HEAD@{0}: checkout: moving from risky to mainb78fe8f HEAD@{1}: commit: Risky work9208aaa HEAD@{2}: checkout: moving from main to riskyb78fe8f Risky work9208aaa Add n3Notice that Git even printed the old hash when it deleted the branch: was b78fe8f. Write that hash down and you can always make the branch again with git branch name b78fe8f.
Limits of the reflog: it is local (it is not pushed, and a new clone has no reflog), it only covers commits that HEAD or a branch pointed to, and old entries are removed after a time. It does not save uncommitted work. git fsck --lost-found can list dangling commits and blobs too; in my test it printed a few dangling commit lines.
Revert: undo a commit on shared history
Reset rewrites history. If the bad commit is already on the shared main, don't rewrite. Add a new commit that does the opposite:
git log --onelinegit revert --no-edit HEAD~1git log --oneline | head -4lsBefore this, the history ended with Add ok file (top) on Add bad feature. The revert of the older of the two:
[main 3ec3bd3] Revert "Add bad feature" 1 file changed, 1 deletion(-) delete mode 100644 bad.txt3ec3bd3 Revert "Add bad feature"28b27de Add ok file6165daf Add bad feature9208aaa Add n3bad.txt was removed, ok.txt is still there, and the bad commit is still in the log. Nothing was rewritten, so it is safe for everyone. The new commit message says This reverts commit 6165daf....
Reverting a merge needs one extra option, because a merge has two parents and Git does not know which side to keep. I tried:
error: commit 4e6804d... is a merge but no -m option was given.fatal: revert failedgit revert -m 1 <merge> means "keep parent 1 (the mainline) and undo what the other side brought". It worked and deleted s.txt, the file the side branch had added.
If you revert a merge, and later merge the same branch again, Git thinks those changes were already applied and skips them. You first have to revert the revert. This is a known trap with reverted merges.
Stash: park unfinished work
You are half-way through something and must switch to an urgent fix. Don't commit half-done code. Stash it:
git status --shortgit stashgit status --shortgit stash list M notes.txt?? scratch.txtSaved working directory and index state WIP on main: 23a5697 Add notes?? scratch.txtstash@{0}: WIP on main: 23a5697 Add notesBy default stash saves changes to tracked files. The untracked scratch.txt stayed behind. Add -u to include untracked files, and -m for a name:
git stash popgit stash push -u -m "wip with scratch"git stash liststash@{0}: On main: wip with scratch(git stash pop first brought back the edit and dropped the stash, Dropped refs/stash@{0}.) Now the working tree is clean. Do the urgent work, switch back, then:
git stash pop re-applies the newest stash and deletes it. git stash apply re-applies but keeps it. git stash list shows all, and git stash drop deletes the newest.
If a pop conflicts with newer changes, Git keeps the stash so you do not lose it.
Cherry-pick: copy one commit
A feature branch has three commits, and the middle one is a small fix that main needs today:
* cc1bd06 Feature: part 2* e866b40 Fix typo in config* 3478903 Feature: part 1* 7f7cecb Basegit switch maingit cherry-pick feature~1git log --oneline --graph --all[main 4753df3] Fix typo in config 1 file changed, 1 insertion(+) create mode 100644 fix.txt* cc1bd06 Feature: part 2* e866b40 Fix typo in config* 3478903 Feature: part 1| * 4753df3 Fix typo in config|/* 7f7cecb BaseThe fix is on main as a new commit (4753df3) with the same change but a different hash, because it has a different parent. Add -x to record the original hash in the message:
Feature: part 1(cherry picked from commit 347890386a9d34cadca50f7fdf5cb2081d34f4ac)If the change does not fit, you get a conflict like in lesson 2. Resolve it and run git cherry-pick --continue, or give up with git cherry-pick --abort. In my run, the conflicted pick showed UU app.txt in git status --short, and --abort made everything clean again.
Use cherry-pick sparingly, for example for a hotfix on a release branch.
Bisect: find the commit that broke it
You know the code worked last week and is broken now. Between them are 13 commits. Instead of reading all of them, let Git do a binary search. It checks out the middle commit, you say good or bad, and it halves the range. With 13 commits this takes about 4 steps (log2(13) is about 3.7).
I made a script test.sh that exits with 0 when calc.sh 2 3 prints 5, and 1 otherwise. A commit called "Refactor calc arithmetic" changed + to - by mistake, in the middle of unrelated commits.
sh test.sh; echo "test exit: $?"git bisect startgit bisect bad # current commit is brokengit bisect good HEAD~13 # this old one workedgit bisect run sh ./test.sh # let Git run the test at each steptest exit: 1status: waiting for both good and bad commitsstatus: waiting for good commit(s), bad commit knownBisecting: 6 revisions left to test after this (roughly 3 steps)[439f3af907d6e01c4589a93b6419818bd1f047b9] Change 7running 'sh' './test.sh'Bisecting: 3 revisions left to test after this (roughly 2 steps)[3ec7d0aa2ffe3079c20c74da97da471aafa3badc] Refactor calc arithmeticrunning 'sh' './test.sh'Bisecting: 0 revisions left to test after this (roughly 1 step)[f0b82dd95e395a2ac2cc62ba4abe783adbec4f1e] Change 9running 'sh' './test.sh'3ec7d0aa2ffe3079c20c74da97da471aafa3badc is the first bad commitcommit 3ec7d0aa2ffe3079c20c74da97da471aafa3badcAuthor: Ada Lovelace <ada@example.com>Date: Thu Jan 1 10:00:00 2026 +0000 Refactor calc arithmetic calc.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)bisect found first bad commitGit tested only three commits and named the guilty one. Without run, you do the testing by hand and type git bisect good or git bisect bad at each step. The script contract (from the Git manual): exit 0 means good, 1 to 127 (except 125) means bad, and 125 means "cannot test this commit, skip it". Always finish with:
git bisect resetThis returns you to the branch you started on. Small, working commits (lesson 1) make bisect precise.
A safety checklist
Run git status before big commands. Commit or stash first, because committed work is recoverable. Make a backup branch (git branch backup-before-rebase) before anything risky. Never rewrite history that others already have; use revert. After a mistake, stop, run git reflog, and think before the next command.
Try it
Use a practice repository with at least four commits.
- Change a tracked file, then discard the change with one command. Then stage a change and unstage it without losing it.
- Make a commit with a wrong message. Fix it. Does the hash change?
- Make three commits. Use
git reset --soft HEAD~3and make them one commit. What does the soft reset leave in the staging area? - Run
git reset --hard HEAD~1. Use the reflog to bring the commit back. What did you type? - Create a branch with one commit, delete it with
-D, and bring the branch back. - Introduce a bug in the middle of ten commits, and find it with
git bisect run. - For each case, say whether you would reset or revert: (a) your own commit that you have not pushed; (b) a commit that is on
mainand was released yesterday.
Answers
git restore filediscards. For the second partgit add file, thengit restore --staged file; the edits stay in the file.git commit --amend -m "New message". Yes, the hash changes, because the message is part of the commit.- All the changes of the three commits, staged and ready. A single
git commit -m "..."creates the combined commit. git reflogshowsHEAD@{1}: commit: ..., thengit reset --hard 'HEAD@{1}'(or the hash).git branch -D nameprintswas <hash>. Recreate it withgit branch name <hash>, or find the hash ingit reflog.- Write a test script that exits 1 when the bug is present. Then
git bisect start,git bisect bad,git bisect good <old>,git bisect run ./test.sh, and finallygit bisect reset. - (a) Reset (or amend) is fine because nobody else has the commit. (b) Revert, because it is shared and released; rewriting it would break everyone's copy.
Interview questions
What is the difference between git reset and git revert? (Must know)
reset moves the branch pointer back, so the commits disappear from that branch's history; it rewrites history. revert adds a new commit that undoes an older one, so history only grows. I use reset for local commits nobody has seen, and revert for anything that is already pushed or released. If I used reset on a shared branch, everyone else would have to repair their copy.
Explain --soft, --mixed and --hard. (Must know)
All three move the branch. --soft leaves the staging area and files alone, so the changes stay staged. --mixed (the default) resets the staging area but keeps the files, so the changes become unstaged. --hard resets both, so uncommitted changes are lost. I use soft to redo a commit, mixed to split a commit, and hard only when I truly want to throw things away.
I ran git reset --hard by mistake. Can I get my work back? (Must know)
If the work was committed, yes. I run git reflog, find the entry from before the reset, and run git reset --hard <hash> or make a branch there. If the changes were never committed or staged, they are almost certainly gone, since Git never stored them. (Changes that were staged with git add have a blob object, and git fsck --lost-found can sometimes find those, but I would not count on it.) That is why I commit or stash before risky commands.
What is the reflog, and how long does it keep entries? (Senior signal)
The reflog is a local log of where HEAD and each branch pointed over time. It is not shared and not part of a clone. Entries expire after a set time. The defaults in the Git config docs (which I read) are 90 days for entries still reachable and 30 days for unreachable ones; teams can change them. It helps with recent mistakes. For long-term safety I rely on pushed branches and tags.
How do you undo a merge that is already pushed? (Senior signal)
git revert -m 1 <merge-commit>, where -m 1 says to keep the first parent (the mainline). Revert creates a new commit, so it is safe. A trap: if I later want to merge the same branch again, Git thinks its changes are already in, so I must revert the revert first. For a merge that was not pushed, git reset --hard ORIG_HEAD is simpler.
When do you use git stash, and what are its limits? (Must know)
When I need a clean working tree to switch tasks, and a commit would be premature. git stash -u -m "name" includes untracked files. pop re-applies and removes the stash, apply keeps it. Limits: it is local, stashes are easy to forget, and long-lived stashes are a smell. For anything longer than a day I prefer a WIP commit on a branch that I later amend or squash.
What does git cherry-pick do and what is the risk? (Senior signal)
It copies a commit's change onto the current branch as a new commit with a new hash. I use it for backporting a fix to a release branch. The risk is that the same change now exists twice on different commits. When the branches merge later it can cause conflicts or confusing history. I add -x so the message records the original hash.
How does git bisect work, and when would you use it? (Senior signal)
It does a binary search over the commits between a known good one and a known bad one. At each step it checks out the middle commit and I mark it good or bad, or a script marks it with git bisect run. For N commits it takes about log2(N) steps, so 1,000 commits need about 10. I use it when I know a regression exists but not where it came from. It works best with small commits that each build.
What is the difference between git restore, git reset and git checkout? (Trap)
restore changes files (working tree and/or staging area). reset moves the branch (and may change the staging area and files). checkout used to do both jobs and also switched branches, which was confusing. Git 2.23 added switch for branches and restore for files. Now the choice is: which thing am I changing, files or the branch pointer?
You amended a commit that you had already pushed. What happens? (Trap)
The amended commit has a new hash. My local branch and the remote branch have diverged, so a normal push is rejected. If I force push, anyone who already pulled the old commit has a problem: their history has a commit that no longer exists on the remote. For a private branch only I use, that is acceptable (with --force-with-lease, lesson 5). For a shared branch, I make a new commit instead, or use revert.
Key takeaways
- Committed work is hard to lose. Uncommitted work is easy to lose. Commit or stash first.
restoreis for files,resetmoves the branch,revertadds an undo commit. Usereverton shared history.reset --softkeeps changes staged,--mixedkeeps them unstaged,--hardthrows them away.git reflogis your undo log forHEADand branches. It is local and it expires.stashparks work (-ufor untracked files),cherry-pickcopies one commit,bisect runfinds the guilty commit.- After a mistake: stop,
git status,git reflog, think, then act.
What's next
Next: Rebase, clean history and team workflows, Lesson 5. You will learn when to rebase instead of merge, how to squash and reword commits, why --force-with-lease is safer than --force, and how teams protect main with branch rules, CODEOWNERS and CI. Practise now: pick a repository, create a backup branch, and try each reset mode on it, then fix it with the reflog.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Related lessons
- Git, Team Workflow and Monorepos · Full-Stack Questions
Part of the study guide
Related series