All articles
169 articles · updated weekly See our Tools
All articles
Tutorials

How to Safely Undo Changes in Git: reset, revert, restore, and reflog

Which Git command to use to undo changes in each situation — reset, revert, restore, stash, and reflog — without losing your work.

How to Safely Undo Changes in Git: reset, revert, restore, and reflog
COVER · Tutorials

You edited the wrong file, committed something half-finished, or ran git add on a file you didn't mean to — and now your stomach tightens before typing the next command. The fear is fair: some Git "undo" commands silently and permanently delete work, while others are perfectly safe. The difference between git checkout, git restore, git reset, and git revert isn't cosmetic — it's the difference between regaining your composure and losing an afternoon of code. This guide shows which command to use in each situation, always favoring the paths that destroy nothing.

Before anything else: find out what state you're in

Almost every undo mistake comes from applying the right command in the wrong state. Git has three areas, and the correct command depends on where your change lives:

  • Working directory: you edited the file but haven't run git add.
  • Staging (index): you ran git add but haven't committed.
  • History (commits): you already ran git commit.

The command that tells you the truth here is git status. Run it before undoing anything:

git status

It literally tells you which command to use ("use git restore to discard changes", "use git restore --staged to unstage"). If you're just getting started and these three states still feel abstract, it's worth going through our Git for beginners guide first, which walks through the add → commit → push cycle calmly. Here the focus is the opposite: how to step back.

Undoing changes that haven't been committed yet

Unstage a file (after git add)

You staged a file by mistake but want to keep the edits. This is safe — nothing is deleted:

git restore --staged file.js     # Git 2.23+
git reset HEAD file.js           # classic form, same effect

The file goes back to "modified but not staged". Your work stays intact.

Discard edits in a file (danger: this deletes)

Here lives the first real trap. If you want to throw away a file's changes and return to the last commit:

git restore file.js              # Git 2.23+
git checkout -- file.js          # old form

This command permanently discards anything uncommitted. There's no reflog for uncommitted edits. Before running it, ask yourself: "am I sure I don't want any of this?". If doubt creeps in, git stash instead of discarding — it tucks everything into a recoverable drawer:

git stash          # stashes your changes and cleans the working directory
git stash pop      # brings them back whenever you want

git stash is the most underrated "undo with a safety net" in Git.

Undoing commits: reset vs. revert

Here's the most important decision in this guide. The golden rule:

Commit you have NOT pushed yet → git reset. Commit you HAVE pushed and shared → git revert.

git reset — for rewriting local history

git reset moves the branch pointer to an earlier commit. It has three modes, and understanding the difference is what prevents lost work:

git reset --soft HEAD~1    # undoes the commit, KEEPS everything staged
git reset --mixed HEAD~1   # (default) undoes the commit, keeps edits in the working dir
git reset --hard HEAD~1    # undoes the commit AND DELETES the changes
  • --soft: the most common case. "I committed too early / bad message." The commit disappears, but your files stay ready for a new commit. Safe.
  • --mixed: the commit disappears and the changes return to the working directory, unstaged. Good for reorganizing what goes into each commit. Safe.
  • --hard: destructive. It deletes the working directory changes. Only use it when you're 100% sure you want none of it.

To fix just the last commit's message, you don't even need a reset:

git commit --amend -m "correct message"

git revert — to undo without rewriting history

git revert deletes nothing: it creates a new commit that undoes the changes of an earlier commit. It's the mandatory choice when the commit is already on the remote and other people may have pulled it.

git revert abc1234           # creates a commit that inverts abc1234

Why not use reset on a shared branch? Because reset rewrites the timeline. If you do reset + push --force on a branch a teammate already pulled, their history and yours diverge, and the next pull becomes a minefield. revert keeps the full history — anyone auditing sees both the mistake and the fix, which is usually what you want.

The safety net almost nobody uses: git reflog

If you ran a reset --hard and your blood ran cold, breathe. Git keeps a log of where HEAD pointed across your recent actions: the reflog. Commits "lost" to a reset still exist for weeks before the garbage collector clears them.

git reflog                          # lists the history of HEAD movements
git reset --hard HEAD@{1}           # go back to where you were before the mistake

The reflog recovers commits that vanished from git log. It does not, however, recover edits that were never committed or stashed — which is why git restore and reset --hard on uncommitted files are the most dangerous commands in the set.

Quick decision table

Situation Safe command
Unstage a file git restore --staged file
Discard an uncommitted edit git stash (recoverable) or git restore file (deletes)
Fix the last commit's message git commit --amend
Undo the last commit, keep the code git reset --soft HEAD~1
Undo a commit already on the remote git revert <hash>
Recover from a reset --hard git reflog + git reset --hard HEAD@{n}

Frequently asked questions

How do I undo the last commit without losing my changes?

Use git reset --soft HEAD~1. The commit is removed from history, but all your changes stay staged, ready for a new commit. If you want the changes back in the working directory (unstaged), use git reset --mixed HEAD~1. In neither case is your code deleted — only --hard does that.

What's the difference between git reset and git revert?

git reset rewrites history: it moves the branch pointer backward and the undone commits vanish from the timeline. It's ideal for local mistakes nobody has seen yet. git revert rewrites nothing — it creates a new commit that cancels an earlier one, preserving the entire history. Use revert whenever the commit has already been pushed to a shared branch.

Do git checkout and git restore do the same thing?

Partly. The old git checkout was overloaded: it both switched branches and discarded file changes, which confused a lot of people. As of Git 2.23, that second job became git restore (discard/restore files) and branch switching became git switch. Functionally git restore file and git checkout -- file are equivalent, but git restore is more explicit and less risky.

Can I recover changes after a git reset --hard?

It depends on the state they were in. If the changes had become a commit, yes: git reflog shows the previous point and you return with git reset --hard HEAD@{1}. If they were edits never committed or stashed, no — they leave no recoverable trace. That's why the recommendation is to stash before any destructive command.

What to take from this guide

Safety in Git doesn't come from memorizing commands — it comes from two questions before you type: "is my change in the working dir, staged, or already committed?" and "has this commit already been pushed?". The first picks between restore and reset; the second, between reset and revert. Reserve --hard and push --force for when you're absolutely certain, use git stash as a net whenever there's doubt, and remember that git reflog is your insurance against lost commits. Undoing in Git stops being scary once you know that, in the vast majority of cases, nothing is truly irreversible.

RD
Author
Rafael Duarte
Backend developer with a background in fintech and B2B SaaS — worked on teams that scaled APIs from zero to millions of requests. Carries enough production scars to hold strong opinions on tools, patterns and architecture decisions. Not an academic: he read the UUID RFC when he had to choose between v4 and v7 for a write-heavy table.
View profile