DEV Community

James Joyner
James Joyner

Posted on Originally published at moderngitacademy.com AI-assisted

Undo a Git Commit: Two Questions Before You Type Anything

"How do I undo a commit?" has four correct answers and three wrong ones, and the difference is not the command — it is two questions you ask before typing anything:

  1. Where is the change? Working tree, staged, committed, or committed and pushed?
  2. Has anyone else got it?

Answer those and the command picks itself. Below is each situation, run in a disposable repository (git 2.43.0) with real output, followed by the one command that will hurt you and how to tell.

The repository has three commits touching one file: Initial config, Add line3, Add line4.

A. You edited a file and want the edit gone

Nothing staged, nothing committed.

echo oops >> config.txt
git status --short
Enter fullscreen mode Exit fullscreen mode
 M config.txt
Enter fullscreen mode Exit fullscreen mode

The M is in the second column: the change lives only in the working tree. The tool for the working tree is restore:

git restore config.txt
git status --short
Enter fullscreen mode Exit fullscreen mode

Empty output — clean. (git checkout -- config.txt is the older spelling of the same thing.)

B. You staged something too early

The edit should stay; it just should not be in the next commit.

echo staged >> config.txt
git add config.txt
git status --short
Enter fullscreen mode Exit fullscreen mode
M  config.txt
Enter fullscreen mode Exit fullscreen mode

First column now: the change is in the index. Move it back without touching the file:

git restore --staged config.txt
git status --short
Enter fullscreen mode Exit fullscreen mode
 M config.txt
Enter fullscreen mode Exit fullscreen mode

Second column again — the edit is still there, just unstaged. git reset config.txt does the same. What you must not do next is git restore config.txt "to be safe": that is situation A's command and it discards the edit you just chose to keep.

C. You committed too early, and nobody has it

The last commit should not exist, but its changes should stay staged so you can recommit properly.

git reset --soft HEAD~1
git status --short
git log --oneline
Enter fullscreen mode Exit fullscreen mode
M  config.txt
76ca1c4 Add line3
333e24f Initial config
Enter fullscreen mode Exit fullscreen mode

The commit is gone from the branch; its content is sitting in the index, ready for a better commit message or a split. --soft is the gentle one: it moves the branch pointer and nothing else. (Without a flag, reset also unstages; with --hard it also discards — see the last section.)

If it was only the message that was wrong, git commit --amend -m "better message" rewrites the tip commit in place. Same rule applies: not if anyone else has it.

D. The commit is pushed and others have pulled it

Now rewriting is off the table. Rewriting removes a commit that your colleagues' branches are built on; their next pull becomes a merge of history you deleted. The right tool adds a commit instead of removing one:

git revert HEAD --no-edit
git log --oneline | head -2
cat config.txt
Enter fullscreen mode Exit fullscreen mode
[main 6a6f2b6] Revert "Add line4"
 1 file changed, 1 deletion(-)
6a6f2b6 Revert "Add line4"
d906de1 Add line4
line1
line2
line3
Enter fullscreen mode Exit fullscreen mode

Both commits are in history: the original and its inverse. Anyone who pulls gets the undo as an ordinary new commit. That is the entire reason revert exists.

The decision, as a table

Where is the change? Anyone else got it? Command
Working tree only git restore <file>
Staged git restore --staged <file>
Committed, not pushed no git reset --soft HEAD~1 (or --amend for the message)
Committed and pushed yes git revert <commit>
Committed and pushed no, you are certain git reset + git push --force-with-lease — and be certain

The one that destroys work

echo unsaved >> config.txt
git status --short
git reset --hard HEAD
git status --short
Enter fullscreen mode Exit fullscreen mode
 M config.txt
HEAD is now at 778de83 Revert "Add line4" — rolled back for the release
Enter fullscreen mode Exit fullscreen mode

The second status prints nothing. The edit is gone, and — this is the part people get wrong — it is not in the reflog:

git reflog | head -2
Enter fullscreen mode Exit fullscreen mode
778de83 HEAD@{0}: reset: moving to HEAD
778de83 HEAD@{1}: commit (amend): Revert "Add line4" — rolled back for the release
Enter fullscreen mode Exit fullscreen mode

The reflog records where HEAD pointed. Uncommitted edits never had a commit, so there is nothing to point at. reset --hard discards every uncommitted change in the repository, not just the one you were thinking about. When you want situation A, use situation A's command; keep --hard for "I mean everything".

Practise it without risking anything

The three situations above — plus the shared-commit one — are a lab you run in /tmp, and the same lab now has a browser Practice Mode: a small Git simulator that checks the repository state, so git reset config.txt and git restore --staged config.txt both count, reset --hard asks you to confirm what it would discard, and rewriting a shared commit is refused with the reason.

Lab: reset, revert and restore — choose the right one

The decision table in full, including amend and interactive rebase for older commits and what each does to everyone else's clone: Undo a Git commit: amend, reset, revert or rebase.

Top comments (0)