DevFox Labs
HomeLearningToolsAboutContact
beginner35 minby DevFox

Undoing Things Safely

A decision guide for every common mess: restore, clean, amend, reset soft/mixed/hard, revert, and reflog rescue — with a clear line between what is recoverable and what is not.

  • Git

On this page

  • The two questions behind every undo
  • "I staged the wrong file"
  • "I want to throw away my edits to a file"
  • "I want to throw away everything uncommitted"
  • "My last commit is wrong"
  • "I want to undo a commit but keep the changes"
  • "The bad commit is already pushed"
  • "I reset and now I want it back"
  • "I want one old version of one file back"
  • The whole map on one page
  • A cheat sheet to keep
  • Where to go next
DevFox Labs

Structured lessons and courses for developers who care about craft.

Platform

HomeLearningAll lessonsAboutContact

Legal

Terms of ServicePrivacy PolicyCookie Policy

© 2026 DevFox Labs. All rights reserved.

    Sooner or later — possibly this afternoon — you will stage a file you did not mean to stage, write a commit message with a typo in it, or make a commit that should never have existed. That is the moment most people quietly panic and start copying files to the desktop "just in case".

    You do not need to. Git has a precise tool for each of these messes. By the end you will know which one to reach for in each situation, and — just as importantly — which of them destroy work permanently and which of them cannot.

    The two questions behind every undo

    Git's undo commands look like a jumble of unrelated words: restore, reset, revert, clean, amend. They get much easier to hold once you see that they differ along only two axes, and that the two axes are two different kinds of trouble.

    Can it destroy unsaved typing?Leaves history aloneRewrites historyTouches your files
    Dangerous to you

    restore <file>, clean -fd. They overwrite work Git never recorded, and Git cannot give back what it was never given.

    Dangerous to everyone

    reset --hard. Both kinds of trouble at once — which is why it is the command with the reputation.

    Leaves your files alone
    Safe

    restore --staged, revert. Nothing is lost and nobody else's copy is disturbed.

    Fine alone, rude in public

    reset --soft, reset, amend. Free at your own desk; genuinely disruptive once somebody else has those commits.

    Whose copy of the project is at risk?
    Everything in this lesson is one box on this grid

    Danger to you, danger to others

    The two axes describe two different kinds of trouble. Touching the working tree risks your unsaved work. Rewriting history risks everyone else's copy of the project. Every command below is a specific combination of the two, and each section says which.

    "I staged the wrong file"

    You ran git add . and swept up a scratch file along with the real change. Nothing is committed yet, so this is harmless to fix:

    bash
    git restore --staged notes-to-self.txt

    The file drops out of the staging area and back into the plain "modified" pile. Your edits to it are untouched — you have only changed Git's answer to "what goes in the next commit?" Neither axis is in play: no working tree changes, no history rewritten.

    You will also see git reset notes-to-self.txt recommended, and it does the same job. It is the older spelling, from before Git had a verb dedicated to files. Prefer git restore: it says what it does, and it will not be confused with the commit-moving reset you meet later in this lesson.

    Why restore and switch exist

    For years, git checkout did three unrelated jobs: switching branches, creating them, and overwriting files from history. One typo could turn "discard my edits" into "move me to another branch". Git 2.23 split it into git switch for branches and git restore for files. Both spellings still work; the new ones are far harder to misfire.

    "I want to throw away my edits to a file"

    You spent twenty minutes going down a wrong path in one file and want it back exactly as it was at the last commit:

    bash
    git restore src/pricing.js

    The file is rewritten from the last commit, and your twenty minutes are gone. Not in a bin, not in a hidden folder — gone. Those edits only ever existed in the file, and you just overwrote the file.

    This is the first command here that can genuinely cost you something, and the rule behind it is worth memorising: Git protects what you have committed, and nothing else.

    Uncommitted work has no undo

    Every recovery trick later in this lesson — reflog included — works by finding a commit again. Changes you never committed were never in Git's database, so there is nothing to find. Before running git restore on work you are unsure about, commit it on the spot; you can always delete the commit later.

    "I want to throw away everything uncommitted"

    Sometimes the experiment is a write-off and you want the whole folder back to the last commit. That takes two commands, because Git treats tracked and untracked files differently. First, reset every tracked file:

    bash
    git restore .

    That leaves behind the files Git has never seen — the stray test-copy.html, the debug script, the folder you made at 2am. git clean removes those, and you should always ask it what it plans to do first:

    bash
    git clean -nd   # -n = dry run, -d = include directories

    Read the list. Every path it prints will be deleted, with no way back. When you are happy with it, swap the -n for -f:

    bash
    git clean -fd   # -f = force, actually delete them

    A third flag, -x, also deletes files matched by your .gitignore. That sounds tidy and is quietly the most destructive option here: .gitignore is exactly where local .env files, credentials, and databases live. Reserve -x for when you truly want a machine-fresh checkout, and dry-run it even then.

    "My last commit is wrong"

    Your commit is made, the message says "Fix pricingg table", and you have not shared it with anyone. Rewrite it:

    bash
    git commit --amend -m "Fix pricing table rounding"

    If you instead forgot to include a file, stage it and amend without touching the message:

    bash
    git add src/pricing.test.js
    git commit --amend --no-edit

    The important part: --amend does not edit the old commit. Commits are immutable, so Git builds a new commit with the corrected content and a new hash, then points your branch at it and orphans the old one.

    That is invisible while the commit lives only on your machine. If you have already pushed it, your history and the shared history now disagree, and the only way to push is to force — which overwrites work for anyone who pulled it.

    Never rewrite history others have

    --amend and reset replace commits rather than adding to them. On a branch that only exists on your laptop, that is free. On a shared branch it detaches your teammates' work from yours and creates conflicts they did not cause. The rule: rewrite freely before you push, never after.

    "I want to undo a commit but keep the changes"

    You committed too early, or bundled two ideas into one commit. You want the commit gone but every line of code kept:

    bash
    git reset --soft HEAD~1

    HEAD~1 means "one commit before where I am now". The commit disappears from your branch and its changes reappear in the staging area, ready to be re-committed however you like.

    To keep the changes but unstage them — back to plain edited files — leave the flag off. --mixed is the default:

    bash
    git reset HEAD~1

    The three flags differ only in how far they reach through the three areas from lesson 3:

    • --soft — moves the branch. Staging area and working tree are left exactly as they are.
    • --mixed (default) — moves the branch and clears staging. Your files keep their content.
    • --hard — moves the branch, clears staging, and overwrites your files to match the target commit.
    bash
    git reset --hard HEAD~1   # discards the commit AND your edits

    git reset --hard is the one command here that destroys both committed and uncommitted work in a single stroke. The commit it drops is recoverable through the reflog. Any uncommitted edits sitting in your working tree when you run it are not.

    Reset moves a label, it does not delete

    A branch is a sticky note pointing at one commit. git reset peels the note off and sticks it elsewhere; the commits it pointed at sit in Git's database, unreferenced, for weeks. That is why the reflog can bring them back — and why --hard is so much worse for uncommitted files, which were never in the database at all.

    "The bad commit is already pushed"

    Now reset is off the table. Other people have that commit; you cannot un-give it to them. What you can do is publish a correction:

    bash
    git revert 9f8e7d6

    git revert reads the commit you name, works out the exact opposite of it, and makes that opposite a new commit on top of your branch. History grows instead of changing, so everyone else pulls one more commit and their copy stays consistent with yours.

    Think of an accounting ledger. You do not scratch out an entry that has already been audited — you post a correcting entry underneath, and the record shows both. reset is the eraser; revert is the correcting entry.

    The choice between them is not about which is nicer. It is about who has seen the commit:

    • Only you have it, and you want it gone → git reset.
    • Anyone else has it → git revert, every time.

    Reverting leaves the mistake visible in the log, and that is a feature. "Revert 'Enable new checkout flow'" tells a month-later reader more than a gap where a commit used to be.

    "I reset and now I want it back"

    You ran git reset --hard HEAD~1, and thirty seconds later realised the commit mattered. Git keeps a private log of every position HEAD has occupied — the reflog:

    bash
    git reflog

    You get something like this, newest first:

    text
    a1b2c3d HEAD@{0}: reset: moving to HEAD~1
    9f8e7d6 HEAD@{1}: commit: Add pricing table
    3e4f5a6 HEAD@{2}: commit: Update README

    Line HEAD@{1} is where you stood before the reset, and 9f8e7d6 is the commit you thought you had destroyed. Put your branch back on it:

    bash
    git reset --hard 9f8e7d6

    Your commit is back. The reflog is local, never pushed, and entries expire after about ninety days — plenty of time to rescue an afternoon. The full tour lives in the intermediate course; this much is enough to get you out of trouble today.

    Commit early so the reflog can save you

    The reflog only knows about commits. Small, frequent commits are not just good hygiene — they are what makes almost every mistake reversible. Before trying something risky, commit, even with a message as rough as "wip: before refactor". You can always tidy it up later with --amend.

    "I want one old version of one file back"

    Occasionally you do not want to undo a commit at all. You only want one file as it was three commits ago, while everything else stays current:

    bash
    git restore --source=9f8e7d6 -- src/pricing.js

    The --source flag says where to read from instead of the last commit, and -- separates the commit from the path so Git never confuses a filename with a branch name. A relative reference like --source=HEAD~3 works just as well.

    The file in your working tree is overwritten immediately, so the uncommitted-work warning applies here too. No history is rewritten — you now have an old version of one file sitting as an ordinary edit, ready to commit if you like it.

    The whole map on one page

    CommandWhat it changesRewrites?Recoverable?
    restore <file>Working treeNoNo
    restore --stagedStaging areaNoYes
    reset --softBranch pointerYesYes (reflog)
    reset (mixed)Pointer + stagingYesYes (reflog)
    reset --hardPointer + both areasYesCommits only
    revert <commit>Adds a new commitNoYes
    clean -fdUntracked filesNoNo

    Read the last column as the real safety rating. Anything marked "No" is a one-way door — and both of those rows are commands that reach into your working tree, which is the top-left corner of the grid at the start of this lesson.

    A cheat sheet to keep

    Every command from this lesson, in the order you are likely to need them:

    bash
    git restore --staged <file>      # unstage, keep the edits
    git restore <file>               # discard edits (NOT recoverable)
    git restore .                    # discard all tracked-file edits
    git clean -nd                    # dry run: list untracked files
    git clean -fd                    # delete them (NOT recoverable)
    git clean -fdx                   # also delete ignored files
    git commit --amend -m "msg"      # rewrite the last message
    git commit --amend --no-edit     # add staged files to last commit
    git reset --soft HEAD~1          # drop commit, keep changes staged
    git reset HEAD~1                 # drop commit, keep changes unstaged
    git reset --hard HEAD~1          # drop commit and all changes
    git revert <commit>              # new commit undoing an old one
    git reflog                       # every position HEAD has held
    git reset --hard <hash>          # jump back to a recovered commit
    git restore --source=<c> -- <p>  # one file from one commit

    Where to go next

    You now have an answer for every everyday mess: unstage with restore --staged, discard with restore or clean, fix the last commit with --amend, unwind commits with reset, undo published ones with revert, and rescue yourself with reflog. Better still, you can tell before you press Enter whether a command is going to cost you anything.

    Next comes branching and merging basics, where undoing gets easier again — a branch keeps a risky experiment somewhere it cannot touch your main line of work, and throwing the whole branch away costs nothing.

    Before you move on, practise in a throwaway repository. Make a few commits, then break things on purpose: amend one, soft reset another, hard reset a third and fish it back out of the reflog. Doing that once when nothing is at stake is worth more than reading this page five times.