AnyLearn
All lessons
Programmingadvanced

Undo: Reset, Revert, Restore, and Getting Work Back

Git's undo commands are notoriously confusing because they are usually memorised as recipes. Read against the object model they separate cleanly: each acts on a different one of the three places content lives. This lesson maps them, covers recovery from the accidents that feel unrecoverable, and states what genuinely cannot be undone.

Updated · AI-authored, review-gated · how lessons are made

Not signed in: your progress and quiz score won't be saved.
Progress1 / 8

Three places, and which command touches which

The undo commands look arbitrary because they are usually learned as recipes for situations. They are regular once you ask a different question: which of the three places is this changing?

The three places from the first lesson are the working directory, the index, and the commit history reached through refs.

                 history        index        working dir
commit             new            -              -
git add             -           updated          -
git restore         -              -           reset to index
git restore --staged -          reset to HEAD     -
git reset --soft  ref moves        -              -
git reset --mixed ref moves     reset to ref      -
git reset --hard  ref moves     reset to ref   reset to ref

That table is the entire confusion, resolved. reset has three modes because there are three places, and each mode goes one place further.

So --soft moves the branch and leaves everything else, which is why it is the way to squash: the changes remain staged, ready to be committed again as one. --mixed, the default, additionally unstages. --hard additionally discards your working files, and is the only one of the three that can lose uncommitted work.

Full lesson text

All 8 steps on one page, for reading, reference, and search.

Show

1. Three places, and which command touches which

The undo commands look arbitrary because they are usually learned as recipes for situations. They are regular once you ask a different question: which of the three places is this changing?

The three places from the first lesson are the working directory, the index, and the commit history reached through refs.

                 history        index        working dir
commit             new            -              -
git add             -           updated          -
git restore         -              -           reset to index
git restore --staged -          reset to HEAD     -
git reset --soft  ref moves        -              -
git reset --mixed ref moves     reset to ref      -
git reset --hard  ref moves     reset to ref   reset to ref

That table is the entire confusion, resolved. reset has three modes because there are three places, and each mode goes one place further.

So --soft moves the branch and leaves everything else, which is why it is the way to squash: the changes remain staged, ready to be committed again as one. --mixed, the default, additionally unstages. --hard additionally discards your working files, and is the only one of the three that can lose uncommitted work.

2. Reset moves a pointer, revert adds a commit

The pair people confuse most are not variants of each other. They do different things and are appropriate in different circumstances.

git reset moves your branch ref to a different commit. History is rewritten in the sense that your branch no longer points at what it did, and the commits it left behind become unreferenced.

git revert creates a new commit whose content undoes an earlier one. Nothing is rewritten; something is added. The original commit stays in history, followed by a commit that reverses it.

git reset --hard HEAD~1     # branch now points one commit earlier
git revert HEAD             # new commit undoing the last one

The choice is entirely about whether anyone else has the commit.

On a local branch, reset is right: nobody is holding the commits you are abandoning, so removing them from your branch harms nothing.

On a shared branch, revert is the only safe option, for the reason the previous lesson established: rewriting produces different objects, and everyone holding the old ones will merge both copies back in.

Revert is also the honest choice for a production incident. "We shipped this and then undid it" is a fact, and a history that shows it is more useful than one pretending the mistake never happened.

3. Choosing an undo

One branch of that diagram is genuinely dangerous, and it is not the one people fear.

reset --hard on committed work is recoverable: the commits are in the object database and the reflog names them. It feels destructive and is not.

git restore on uncommitted working directory changes is a real deletion. Those changes were never hashed into an object, so there is nothing to recover and no reflog entry. The same applies to git checkout -- <file> and to git clean.

So the actual rule inverts the intuition: the dangerous commands are the ones that touch content Git has never seen. Once something is committed, or even just staged, it exists as an object and is recoverable. Before that it exists only on disk.

The practical instruction that follows is to commit early and often on a local branch, including work in progress. A commit is cheap, private until pushed, and it converts uncommitted work into something the object database will hold onto. Tidy the history afterwards with an interactive rebase.

flowchart TD
A["Something needs undoing"] --> B["Is it committed?"]
B --> C["No, only in the working directory"]
B --> D["Yes, and only local"]
B --> E["Yes, and already pushed"]
C --> F["git restore: discards, unrecoverable"]
D --> G["git reset: moves the ref, reflog holds the old one"]
E --> H["git revert: adds a commit, rewrites nothing"]
H --> I["Safe for everyone who already pulled"]

4. Getting work back

Almost every recovery is the same two steps: find the hash, point something at it.

Deleted a branch. Its tip commit is in your reflog.

git reflog                       # find the hash
git branch recovered <hash>      # a new file, pointing at it

Reset too far. Same procedure: HEAD@{1} is where you were before the reset.

Bad rebase. The pre-rebase state is in the reflog, and ORIG_HEAD also holds it, since Git sets that before any operation that moves HEAD substantially.

Amended away a commit. The original is unreferenced but present, and appears in the reflog.

Lost a stash. Stashes are commits. git fsck --unreachable lists objects nothing points at, and a dropped stash is among them.

The general tool is git fsck --lost-found, which reports every unreachable object. It is verbose and it is the last resort that works when the reflog does not have what you need, for instance recovering a blob that was staged and then overwritten.

The recurring structure is worth naming: Git does not delete on your behalf. Recovery is a search problem rather than a restoration problem, and the reflog is the index you search.

5. Rewriting your own history

Interactive rebase is the tool for making a messy local branch presentable before anyone sees it.

git rebase -i HEAD~5

It opens a list of the last five commits, each with a verb you can change: pick keeps it, reword edits the message, squash folds it into the previous one, drop removes it, and reordering lines reorders the commits.

Understood through the object model there is nothing new here. Each surviving commit is replayed to produce a new object with a new hash, exactly as in the previous lesson. Squashing means applying two diffs and creating one commit; dropping means not replaying that one.

The value is real. A branch containing "wip", "fix typo", "actually fix it" and "revert that" is noise for a reviewer, and one coherent commit is genuinely easier to review, to bisect, and to revert later.

The constraint is the same as before, and it is the only rule: do this before pushing, or only on branches nobody else has. After that, every commit you rewrite is one someone may be holding.

A note on --force-with-lease over --force: it refuses the push if the remote has moved since you last fetched, which catches the case where a colleague pushed while you were rebasing. It costs nothing and prevents the worst outcome, so it should be the default.

6. Finding which commit broke it

The object model enables one tool worth knowing on its own, because it turns an open-ended debugging problem into a bounded one.

git bisect performs a binary search over history. You mark a commit known bad and one known good, and Git checks out the midpoint; you test and say which it is; it halves the range and repeats.

git bisect start
git bisect bad                 # current commit is broken
git bisect good v1.2.0         # this release was fine
# test, then: git bisect good | git bisect bad
git bisect reset

Because the search is binary, a thousand commits take about ten tests. That is the difference between a feasible afternoon and an infeasible one.

It can be fully automated by supplying a script whose exit status decides:

git bisect run ./test.sh

Git then runs the whole search unattended and reports the first bad commit.

This works because checking out any commit costs the same, which is the storage property from the first lesson. In a system that reconstructs versions by replaying diffs, jumping around history like this is far more expensive, and the technique is correspondingly less attractive.

It is also the strongest practical argument for small, coherent commits: bisect identifies a commit, and its usefulness is proportional to how little that commit contains.

7. What genuinely cannot be undone

A short list, and it is short.

Uncommitted changes discarded. restore, checkout --, clean and reset --hard on files Git never hashed. There is no object and no reflog entry. Gone.

Anything after garbage collection. Unreachable objects are removed once the reflog entry expires and collection runs. The window is weeks, not forever.

A pushed secret. Covered in the refs lesson and worth repeating because the instinct is wrong. Rewriting history creates new commits; the old object remains on the remote until its own collection, is fetchable by hash, and is already in every clone taken since. Rotate the credential. Treat the history rewrite as tidying, not as remediation.

Someone else's clone. You cannot reach into it. Every rewrite is advisory to everyone but you.

Everything else is recoverable, and the reason is the property established in the first lesson: objects are named by their content and are therefore immutable, so operations add objects and move pointers rather than changing anything.

That single design decision is what makes Git's undo story unusually strong, and what makes the exceptions above exactly the cases where content never became an object.

8. What the path establishes

CommandActs onRecoverable?
git restore <file>Working directoryNo
git restore --stagedIndexThe blob survives
git reset --softRef onlyYes, reflog
git reset --hardRef, index, working dirCommits yes; uncommitted edits no
git revertAdds a commitNothing to recover
git rebase -iCreates new commitsYes, reflog
git cleanUntracked filesNo

Four lessons, one idea. Git is an immutable content-addressed object store with a set of mutable pointers into it. Every command either adds objects or moves a pointer.

Once that is in place, the rest follows without memorisation. Branches are cheap because they are files. History is tamper-evident because hashes chain. Merging works because parents give a common ancestor. Rebase creates new commits because a commit's hash includes its parent. And recovery is a search rather than a restoration, because nothing was deleted.

The transferable idea is bigger than Git: content addressing turns identity into a checksum, and immutability turns undo into a naming problem. That is the same construction behind the Merkle trees in the cryptography path and behind the deduplication in content-addressed storage generally.

The commands remain awkward. The model does not.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. Why does `git reset` have three modes?
    • Because there are three places content lives, and each mode goes one further
    • Because it supports three different merge strategies
    • Because branches, tags and HEAD each need separate handling
    • Because it must handle local, staged and remote state
  2. When is `git revert` the correct choice over `git reset`?
    • When the change is only in the working directory
    • When the commit has been pushed and others may hold it
    • When you want to remove the commit from history entirely
    • When the commit is a merge commit
  3. Which operation is genuinely unrecoverable?
    • `git reset --hard` moving a branch back several commits
    • Deleting a branch that had unmerged work
    • An interactive rebase that squashed the wrong commits
    • `git restore` on uncommitted working directory changes
  4. Why does `git bisect` work well in Git specifically?
    • Because commits store diffs, making replay fast
    • Because the reflog records which commits were tested
    • Because checking out any commit costs the same, since commits point at whole trees
    • Because binary search requires a linear history
  5. What should you do after accidentally pushing a credential?
    • Rotate the credential; the old object persists on the remote and in every clone taken since
    • Rewrite history and force-push, which removes it
    • Run garbage collection on the remote
    • Delete and recreate the branch

Related lessons

Programming
advanced

Merge and Rebase: Two Answers to Divergence

Every operation so far moved a pointer. Merging is the first that has to decide something: two branches changed the same project and one result must come out. This lesson covers the three-way merge and the merge base it depends on, what a conflict actually is, and why rebase produces different commits rather than moving them.

8 steps·~12 min
Programming
advanced

Refs: A Branch Is a File Containing a Hash

Hashes are unusable by hand, so Git names them. A branch is not a copy of anything, not a directory, and not a container for commits: it is a forty-character file. Once that is clear, checkout, reset, detached HEAD and the reflog stop being separate concepts and become one operation on one kind of file.

8 steps·~12 min
Programming
intermediate

The Object Database: Git Is a Content-Addressed Store

Git is usually taught as a set of commands. Underneath it is a key-value store where the key is a hash of the content, and four object types built on that one idea. This lesson derives the hash by hand, shows what a commit actually contains, and explains why the design makes history tamper-evident.

8 steps·~12 min
Programming
advanced

The Failure Modes the Design Creates

Reconciliation buys robustness and charges for it in a specific currency: nothing is ever definitely finished, commands succeed without meaning anything worked, and manual fixes are silently undone. This lesson covers the failures that come from the model rather than from bugs, how to debug them, and when the whole thing is the wrong tool.

8 steps·~12 min