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.

Lesson 0410 min read#git#undo#recovery

On this page

  1. Words used in this lesson
  2. Start here: what do I want to undo?
  3. Restore: undo edits to files
  4. Amend: fix the last commit
  5. Reset: move the branch
  6. Reflog: recover a lost commit
  7. Revert: undo a commit on shared history
  8. Stash: park unfinished work
  9. Cherry-pick: copy one commit
  10. Bisect: find the commit that broke it
  11. A safety checklist
  12. Try it
  13. Interview questions
  14. Key takeaways
  15. 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 HEAD and each branch pointed. Entries expire after some time (the defaults in Documentation/config/gc.adoc are 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...CommandSafe on shared history?
Throw away edits in a file (not committed)git restore filen/a (local only)
Unstage a file but keep the editsgit restore --staged filen/a (local only)
Get an old version of one filegit restore --source=<commit> filen/a
Fix the last commit message or add a forgotten filegit commit --amendNo, if already pushed
Un-commit but keep the changes stagedgit reset --soft HEAD~1No, if already pushed
Un-commit and keep the changes unstagedgit reset HEAD~1 (mixed)No, if already pushed
Un-commit and throw the changes awaygit reset --hard HEAD~1No, and dangerous
Undo a commit that others already havegit revert <commit>Yes
Recover a "lost" commit or deleted branchgit reflog, then git branch name <hash>n/a
Park unfinished work for latergit stashn/a (local only)
Copy one commit to another branchgit cherry-pick <commit>Yes (makes a new commit)
Find which commit broke somethinggit bisectn/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.

Shell
echo "oops" >> app.txtgit status --shortgit restore app.txtcat app.txtgit status --short
TEXT
 M app.txtv1

The 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):

Shell
echo "staged mistake" > secret.txtgit add secret.txtgit status --shortgit restore --staged secret.txtgit status --short
TEXT
A  secret.txt?? secret.txt

To 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:

Shell
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):

Shell
echo "bb" > bb.txt && git add bb.txtgit commit --amend --no-editgit log --oneline -3git show --stat --format=%s HEAD
TEXT
49924a8 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:

ModeBranch movesStaging areaWorking files
--softYesUnchanged (your changes stay staged)Unchanged
--mixed (default)YesReset to the new commitUnchanged (changes become unstaged)
--hardYesResetReset: 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".

Shell
git reset --soft HEAD~1git status --shortgit log --oneline | head -2
TEXT
A  f3.txt6bd2f39 Add f2892a13b Add f1

Mixed (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".

Shell
git commit -qm "Add f3 again"git reset HEAD~1git status --short
TEXT
?? f3.txt

Hard: I committed again, then ran:

Shell
git reset --hard HEAD~1git status --shortls
TEXT
HEAD is now at 6bd2f39 Add f2a.txtapp.txtb.txtbb.txtf1.txtf2.txt

f3.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.

Shell
git log --onelinegit reset --hard HEAD~2git log --onelinels
TEXT
9208aaa Add n34ff95ea Add n2e05f10d Add n1HEAD is now at e05f10d Add n1e05f10d Add n1n1.txt

n2 and n3 seem gone. Look at the diary:

Shell
git reflog
TEXT
e05f10d HEAD@{0}: reset: moving to HEAD~29208aaa HEAD@{1}: commit: Add n34ff95ea HEAD@{2}: commit: Add n2e05f10d HEAD@{3}: commit (initial): Add n1

The commits are right there. HEAD@{1} is "where HEAD was one move ago". Go back:

Shell
git reset --hard 'HEAD@{1}'git log --onelinels
TEXT
HEAD is now at 9208aaa Add n39208aaa Add n34ff95ea Add n2e05f10d Add n1n1.txtn2.txtn3.txt

Everything is back. The same trick recovers a deleted branch. I made a branch risky with one commit, switched away and deleted it with -D:

Shell
git branch -D riskygit reflog | head -3git branch rescued 'HEAD@{1}'git log --oneline rescued | head -2
TEXT
Deleted 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 n3

Notice 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:

Shell
git log --onelinegit revert --no-edit HEAD~1git log --oneline | head -4ls

Before this, the history ended with Add ok file (top) on Add bad feature. The revert of the older of the two:

TEXT
[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 n3

bad.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:

TEXT
error: commit 4e6804d... is a merge but no -m option was given.fatal: revert failed

git 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:

Shell
git status --shortgit stashgit status --shortgit stash list
TEXT
 M notes.txt?? scratch.txtSaved working directory and index state WIP on main: 23a5697 Add notes?? scratch.txtstash@{0}: WIP on main: 23a5697 Add notes

By default stash saves changes to tracked files. The untracked scratch.txt stayed behind. Add -u to include untracked files, and -m for a name:

Shell
git stash popgit stash push -u -m "wip with scratch"git stash list
TEXT
stash@{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:

TEXT
* cc1bd06 Feature: part 2* e866b40 Fix typo in config* 3478903 Feature: part 1* 7f7cecb Base
Shell
git switch maingit cherry-pick feature~1git log --oneline --graph --all
TEXT
[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 Base

The 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:

TEXT
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.

Shell
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 step
TEXT
test 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 commit

Git 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:

Shell
git bisect reset

This 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.

  1. Change a tracked file, then discard the change with one command. Then stage a change and unstage it without losing it.
  2. Make a commit with a wrong message. Fix it. Does the hash change?
  3. Make three commits. Use git reset --soft HEAD~3 and make them one commit. What does the soft reset leave in the staging area?
  4. Run git reset --hard HEAD~1. Use the reflog to bring the commit back. What did you type?
  5. Create a branch with one commit, delete it with -D, and bring the branch back.
  6. Introduce a bug in the middle of ten commits, and find it with git bisect run.
  7. 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 main and was released yesterday.

Answers

  1. git restore file discards. For the second part git add file, then git restore --staged file; the edits stay in the file.
  2. git commit --amend -m "New message". Yes, the hash changes, because the message is part of the commit.
  3. All the changes of the three commits, staged and ready. A single git commit -m "..." creates the combined commit.
  4. git reflog shows HEAD@{1}: commit: ..., then git reset --hard 'HEAD@{1}' (or the hash).
  5. git branch -D name prints was <hash>. Recreate it with git branch name <hash>, or find the hash in git reflog.
  6. 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 finally git bisect reset.
  7. (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.
  • restore is for files, reset moves the branch, revert adds an undo commit. Use revert on shared history.
  • reset --soft keeps changes staged, --mixed keeps them unstaged, --hard throws them away.
  • git reflog is your undo log for HEAD and branches. It is local and it expires.
  • stash parks work (-u for untracked files), cherry-pick copies one commit, bisect run finds 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

Part of the study guide

Related series