← Back to Blog

The Complete Guide to Undoing Things in Git: amend, reset, revert & reflog

The Scenario Matrix: Find Yourself First

Undo commands depend on three questions: which layer the change sits in (working tree / index / commits), whether it is pushed, and whether collaborators exist. Everything below was verified in a real throwaway repository (outputs in the Reproducible Results section), including one often-missed edge: how untracked files interact with reset --hard.

Scenario Command Risk
Just committed, typo in message git commit --amend Unpushed = zero risk
Undo an unpushed commit, keep changes git reset --soft/--mixed HEAD~1 Zero risk (recoverable via reflog)
Undo an unpushed commit, discard everything git reset --hard HEAD~1 Uncommitted tracked changes are lost too
Undo a pushed commit git revert <hash> Zero risk (adds an inverse commit)
Recover commits after a reset git reflog + git branch rescue <hash> Expires (90 days by default)
Accidentally deleted an untracked new file Git cannot help It never entered the object database

The last row is worth underlining up front: Git can only recover content that entered the object database. Untracked files (created but never git added) are beyond recovery once deleted — verified experimentally below.


amend: Fix the Last Commit (Message and Content)

git commit --amend folds the current staging area and a new message into a brand-new commit replacing the last one:

echo v1 > a.txt && git add . && git commit -qm "initail commmit"   # typo
git commit --amend -qm "initial commit"
git log --oneline        # → ad04563 initial commit (hash changed)

The test confirms both effects: the typo is fixed, and the hash is completely different — amend is "a new commit replaces the old one", not an edit. It is therefore only for unpushed commits; amending a pushed one is history rewriting.

amend also folds in whatever is currently staged: if the index holds changes you do not want committed, unstage them with git reset first.


reset: Where the Changes Land in Each Mode

git reset moves the branch pointer back to a target commit; the three modes differ in where your changes end up. Tested by committing a.txt, then b.txt, then c.txt, and walking back from add-c one mode at a time:

Mode HEAD Index Working tree Tested git status
--soft HEAD~1 Back 1 Kept (c.txt staged) Kept A c.txt
--mixed HEAD~1 (default) Back 1 Cleared Kept (unstaged) ?? b.txt ?? c.txt
--hard HEAD~1 Back 1 Cleared Discarded (tracked files) Clean

One-line selection: soft to re-organize commits, mixed to re-edit, hard to truly discard.

A real edge case from the test: reset --hard does not delete untracked files. In the run above, b.txt had become untracked after mixed, and the subsequent hard reset left it on disk — untracked files are outside both the object database and the index. To clear them too, use git clean -fd (preview with git clean -nd first).

This yields a counter-intuitive safety note: hard reset does not guarantee a clean working tree — untracked and ignored files survive. Conversely, they also survive an accidental hard reset.


revert: The Only Safe Undo for Pushed Commits

git revert <hash> moves no pointers — it computes the inverse patch of the target commit and creates a new commit:

git revert --no-edit HEAD
# → [main 47d7e82] Revert "add-b"
git log --oneline | head -2
#   47d7e82 Revert "add-b"
#   6c3764a add-b

The test confirms: b.txt added by add-b is removed after the revert, and both commits remain in history — nothing was rewritten, and everyone who pulls lands in a consistent state. The cost is an extra "undo record" in history, which is exactly where auditability comes from.

The selection rule follows: pushed (especially on shared branches) → revert; local only → reset. Resetting a shared branch and force-pushing imposes your history on every collaborator — one of the most common team incidents (full comparison in Git Rebase vs Merge).


reflog: Recovering Commits You Thought Were Gone

Commits removed by git reset --hard are not immediately gone — git reflog records every HEAD movement (kept 90 days by default). The tested recovery flow:

git reset --hard HEAD~1          # oops: dropped add-b
git log --oneline | head -1      # → 6c3764a add-b (current HEAD)
git reflog | head -4
#   6c3764a HEAD@{0}: reset: moving to HEAD~1
#   47d7e82 HEAD@{1}: revert: Revert "add-b"   ← the commit to recover is still here
git branch rescue 47d7e82        # one command to recover
git log rescue --oneline | head -1
# → 47d7e82 Revert "add-b" ✓

Three caveats verified along the way: reflog is local (not transferred by push, so teammates have no record of your movements); expired entries are garbage-collected; and uncommitted changes are not in it — they never entered the object database, which is the real meaning of "commit first, then experiment".


Reproducible Results

All outputs below come from a single throwaway repository (commands run verbatim):

① amend fixes the message:

echo v1 > a.txt && git add . && git commit -qm "initail commmit"
git commit --amend -qm "initial commit"
git log --oneline
# → ad04563 initial commit   (typo fixed, hash changed)

② reset in three modes:

# walking back from add-c, one mode at a time
git reset --soft HEAD~1
#   log: 6c3764a add-b        status: A  c.txt        ← change is staged
git reset --mixed HEAD~1
#   log: ad04563 init commit  status: ?? b.txt ?? c.txt ← change is in the working tree
git reset --hard HEAD~1
#   log: ad04563 init commit  status: clean (tracked files)
#   ⚠️ b.txt (untracked) still exists — hard does not clear untracked files

③ revert creates the inverse commit:

git revert --no-edit HEAD
# → [main 47d7e82] Revert "add-b"
git log --oneline | head -2
#   47d7e82 Revert "add-b"   ← the inverse sits on top
#   6c3764a add-b            ← the original is fully preserved

④ reflog recovery:

git reset --hard HEAD~1          # accidentally dropped the revert
git reflog | head -2
#   6c3764a HEAD@{0}: reset: moving to HEAD~1
#   47d7e82 HEAD@{1}: revert: Revert "add-b"
git branch rescue 47d7e82        # recovered ✓

Takeaways

Principle One line
Commit before experimenting Uncommitted changes have no recovery path; once committed, reflog is your safety net
Private reset, shared revert Draw the line at "is it pushed / does anyone collaborate", not at which command feels easier
amend for unpushed only The hash changes; pushed commits get revert
untracked is the blind spot reset --hard neither cleans nor saves it; git add new files early
force push with lease --force-with-lease refuses when the remote moved on

Command reference: Git commands and the git-memo tool; the full merge-strategy comparison in Git Rebase vs Merge.

Advertisement

Frequently Asked Questions

Can I recover my code after git reset --hard?

**Committed work: yes** — `git reflog` records every HEAD movement; find the pre-reset hash and run `git branch rescue <hash>` (reflog keeps 90 days by default). **Uncommitted work: no** — reset --hard wipes tracked working-tree changes with no recovery path (ironically, brand-new untracked files survive, as tested in this article). That is why 「commit first, then experiment」 saves you.

Can I undo commits that are already pushed with reset?

**Technically yes, socially no.** reset rewrites history and then requires a force push, which scrambles every collaborator's local branch and breaks CI records and issue links. For pushed commits use **git revert**: it creates an inverse commit, history stays intact, and everyone who pulls lands in a consistent state. One-line rule: **reset for private branches, revert for shared ones**.

What exactly differs between the three git reset modes?

The difference is **which layer the changes land in**: `--soft` moves only HEAD, keeping changes **staged** (for re-organizing commits); `--mixed` (default) moves HEAD and unstages, keeping changes **in the working tree** (for re-editing); `--hard` moves HEAD and **discards all tracked changes** (for truly giving up — with care). The tested comparison is in the table in this article.

Does git commit --amend create a new commit?

It creates a **brand-new commit** replacing the old one — possibly identical content, but a completely different hash (the timestamp changes). For unpushed commits this is a safe edit; for pushed ones, push gets rejected afterwards and needs force-pushing with all the collaborator problems that entails. Also note: amend folds whatever is currently staged into the commit — check the staging area first.

← Back to Blog