Undo Changes in Git: Restore, Revert, Reset and Reflog Explained
DevOpsLesson 4 of 47 min read
What you'll learn
- Choose between restore, amend, revert and reset for a given mistake
- Explain soft, mixed and hard reset in terms of the three areas
- Recover lost commits and deleted branches with git reflog
- Fix a pushed commit without damaging teammates' history
On this page
You run git reset --hard to clean up and the file you spent the afternoon on is gone. Whether it can come back depends on one question: was it ever committed? Git is forgiving about committed work and ruthless about uncommitted work, and most scary-looking undo problems are solved by asking which of the two you have. This lesson uses a household budget repository to walk through every undo tool, from the gentlest to the dangerous one.
Pick the tool by the situation
| Situation | Reach for | Rewrites history? |
|---|---|---|
| Edited a file, want the old version back | git restore <file> | No (but the edit is lost for good) |
| Staged a file by mistake | git restore --staged <file> | No |
| Last commit has a typo or missed a file | git commit --amend | Yes, replaces the last commit |
| A pushed commit was wrong | git revert <commit> | No, adds a new commit |
| Local commits should vanish or be redone | git reset | Yes, moves the branch |
| You reset too far or deleted a branch | git reflog | No, it is how you recover |
restore: undo uncommitted edits
The repository has three commits, one per month. We scribble a wrong line, check it, and throw it away. Then we stage the same wrong line and unstage it, and finally pull an older version of the file out of history.
$ git log --oneline
483c811 Add March budget with new rent
0834846 Add February budget
d68bad5 Add January budget
$ echo 'mar,coffee,9999' >> budget.csv
$ git status -s
M budget.csv
$ git restore budget.csv
$ git status -s
$ echo 'mar,coffee,9999' >> budget.csv
$ git add budget.csv
$ git status -s
M budget.csv
$ git restore --staged budget.csv
$ git status -s
M budget.csv
$ git restore budget.csv
$ git restore --source=HEAD~2 budget.csv
$ cat budget.csv
month,item,amount
jan,rent,900
jan,groceries,310
$ git restore budget.csv
$ git status -sgit restore budget.csv copies the version from the index (or, if nothing is staged, the last commit) over your file. --staged only moves the index; your edits stay. --source=HEAD~2 reads from two commits back, here the January version, and was undone by a plain restore.
amend: fix the last commit
A typo in a message, or a file you forgot to add, is easy to fix while the commit is still local.
$ echo 'mar,internet,40' >> budget.csv
$ git commit -am "Add internet bil"
[main 6c63353] Add internet bil
1 file changed, 1 insertion(+)
$ git commit --amend -m "Add internet bill"
[main e8964b0] Add internet bill
Date: Mon Oct 5 08:54:52 2026 +0530
1 file changed, 1 insertion(+)
$ git log --oneline -3
e8964b0 Add internet bill
483c811 Add March budget with new rent
0834846 Add February budgetThe hash changed (6c63353 to e8964b0) because an amended commit is a new commit that replaces the old one. That is harmless locally and a problem once the commit is pushed, as the last section shows.
revert: undo a commit by adding another
Suppose someone pushed a bad commit that set March rent to zero. You cannot delete it from shared history, but you can add a commit that applies the opposite change.
# edit budget.csv: change "mar,rent,950" to "mar,rent,0" (a wrong fix)
$ git commit -am "Fix rent typo"
[main 3ec2e02] Fix rent typo
1 file changed, 1 insertion(+), 1 deletion(-)
$ git revert --no-edit HEAD
[main 2c6be42] Revert "Fix rent typo"
Date: Mon Oct 5 08:54:53 2026 +0530
1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline -4
2c6be42 Revert "Fix rent typo"
3ec2e02 Fix rent typo
e8964b0 Add internet bill
483c811 Add March budget with new rent
$ tail -n 3 budget.csv
mar,rent,950
mar,groceries,320
mar,internet,40The bad commit stays in the log and the revert sits on top of it, so everyone's history remains consistent. This is the right tool for anything already pushed. Reverting a merge commit needs an extra -m option to choose a parent; read the docs for that case before trying it.
reset: move the branch pointer
git reset <commit> moves the current branch to another commit. The mode decides what happens to the index and your files.
| Mode | Branch moves | Index | Working tree |
|---|---|---|---|
--soft | Yes | Left as is (changes stay staged) | Left as is |
--mixed (default) | Yes | Reset to match the new commit | Left as is (changes stay as edits) |
--hard | Yes | Reset | Reset: uncommitted changes are destroyed |
$ echo 'apr,electricity,65' >> budget.csv
$ git commit -am "Add April electricity"
[main b511d14] Add April electricity
1 file changed, 1 insertion(+)
$ git reset --soft HEAD~1
$ git status -s
M budget.csv
$ git reset
Unstaged changes after reset:
M budget.csv
$ git status -s
M budget.csv
$ git restore budget.csv
$ git status -sAfter --soft, the April line is staged as if you had never run git commit. Plain git reset unstages it, leaving an unstaged edit. The git restore at the end discarded the edit, so the third form, --hard, would have done all of that in one go and with no warning. Use reset for commits that only exist on your machine.
reflog: the safety net
Git records every place HEAD has been in the reflog, a local diary of moves. Commits you can no longer reach from a branch are still in the object store for a while, and the reflog tells you their hashes. Below, a hard reset appears to delete a commit, and then a deleted branch loses its only commit. Both are recovered in one command.
$ echo 'apr,electricity,65' >> budget.csv
$ git commit -am "Add April electricity"
[main 6e7d40a] Add April electricity
1 file changed, 1 insertion(+)
$ git reset --hard HEAD~1
HEAD is now at 2c6be42 Revert "Fix rent typo"
$ tail -n 2 budget.csv
mar,groceries,320
mar,internet,40
$ git reflog -3
2c6be42 HEAD@{0}: reset: moving to HEAD~1
6e7d40a HEAD@{1}: commit: Add April electricity
2c6be42 HEAD@{2}: reset: moving to HEAD
$ git reset --hard HEAD@{1}
HEAD is now at 6e7d40a Add April electricity
$ tail -n 2 budget.csv
mar,internet,40
apr,electricity,65
$ git switch -c savings-plan
Switched to a new branch 'savings-plan'
$ echo 'savings,goal,5000' >> budget.csv
$ git commit -am "Add savings goal"
[savings-plan 33a1440] Add savings goal
1 file changed, 1 insertion(+)
$ git switch main
Your branch is ahead of 'origin/main' by 4 commits.
(use "git push" to publish your local commits)
Switched to branch 'main'
$ git branch -D savings-plan
Deleted branch savings-plan (was 33a1440).
$ git reflog -4
6e7d40a HEAD@{0}: checkout: moving from savings-plan to main
33a1440 HEAD@{1}: commit: Add savings goal
6e7d40a HEAD@{2}: checkout: moving from main to savings-plan
6e7d40a HEAD@{3}: reset: moving to HEAD@{1}
$ git branch savings-plan HEAD@{1}
$ git log --oneline -2 savings-plan
33a1440 Add savings goal
6e7d40a Add April electricityHEAD@{1} means where HEAD was one move ago. The same trick revives a deleted branch: find the last commit in the reflog and create a branch at it. The reflog is local to your clone, and old entries are eventually cleaned up (the retention is configurable, and is measured in weeks), so recover soon after a mistake.
Amending after you pushed
Amend, reset and rebase all create new commits with new hashes. If the old commit is already on the server, the server sees your new history as a fork and refuses it.
$ echo 'apr,water,22' >> budget.csv
$ git commit -am "Add water bil"
[main 48a4afd] Add water bil
1 file changed, 1 insertion(+)
$ git push
To ../budget-origin.git
483c811..48a4afd main -> main
$ git commit --amend -m "Add water bill"
[main d0f643c] Add water bill
Date: Mon Oct 5 08:54:56 2026 +0530
1 file changed, 1 insertion(+)
$ git push
To ../budget-origin.git
! [rejected] main -> main (non-fast-forward)
error: failed to push some refs to '../budget-origin.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
$ git push --force-with-lease
To ../budget-origin.git
+ 48a4afd...d0f643c main -> main (forced update)The first push was rejected as non-fast-forward. --force-with-lease overwrites the remote branch only if it still points where you last saw it, so you cannot flatten a teammate's new commit by accident; a bare --force has no such check. Reserve either one for a branch only you use, and prefer git revert for anything others may have pulled.
Common mistakes
- Running `git reset --hard` with uncommitted work. Symptom: edits disappear and
git statusis clean. Committed work is recoverable throughgit reflog; uncommitted work is not. Commit or stash first. - Using `reset` on pushed commits. Symptom:
! [rejected] ... (non-fast-forward), or after a force push, teammates see their pulls break. Usegit revertfor shared history. - Amending a commit someone already pulled. Symptom: duplicate commits and confusing conflicts after their next pull, because the old and new versions coexist.
- Assuming a deleted branch is gone. Symptom: panic after
git branch -D. Rungit reflog, find the commit andgit branch <name> <hash>brings it back.
How this shows up in projects and interviews
Everyone eventually commits to the wrong branch or pushes something they should not have. The professional move is to stop, check git status and git reflog, and choose the least destructive tool: revert for shared history, reset for private. Interviews like to ask the difference between revert and reset, or what to do if you ran git reset --hard by mistake; the right answer mentions the reflog and the committed-versus-uncommitted line. Teams also expect you to say when you rewrite a branch rather than doing it silently; see Collaborating in a Team for the habit of telling people what you changed.
Try it yourself
In a practice repository, make four commits. Reset --hard to the second one, then use git reflog to bring the fourth back. Then delete a feature branch with git branch -D and recover it. Write one sentence for each step saying which of the three areas changed.
Show a hint
git reflog lists entries as HEAD@{n}. After recovering a deleted branch, check with git log --oneline <branch> that its commits are back.
Key takeaways
- Committed work is almost always recoverable; uncommitted work thrown away with restore or reset --hard is not.
- Use git revert for commits that are shared, and git reset or amend only for local history.
- reset --soft, --mixed and --hard differ in whether they touch the index and your working files.
- git reflog lists where HEAD has been, which brings back reset commits and deleted branches.
- After rewriting a pushed branch use --force-with-lease, and only on branches nobody else uses.
Test yourself
A trick from this lesson's ride: Shared history? Revert adds a correction; it never rewrites. Four more tricks like it wait along the route, each followed by a question.
Frequently asked questions
How do I undo the last commit in Git?
If it is only on your machine, git reset --soft HEAD~1 undoes the commit but keeps the changes staged, and git reset HEAD~1 keeps them as unstaged edits. To fix just the message or add a missed file, use git commit --amend. If the commit is already pushed, use git revert HEAD instead, which adds a new commit.
What is the difference between git revert and git reset?
git revert creates a new commit that undoes an earlier one and leaves history intact, so it is safe for shared branches. git reset moves the branch pointer back and discards commits from the branch, which rewrites history and is intended for local work. If others have the commits, revert is the right choice.
How do I recover a deleted branch or lost commit in Git?
Run git reflog to see where HEAD has recently been, find the hash of the lost commit, and create a branch there with git branch <name> <hash> or reset to it. This works for commits that were committed before the mistake; uncommitted changes cannot be recovered this way.