Shreyansh Jain.

Software engineer and IIT Roorkee alumnus. I write about programming, computer science, and the things I learn while building software.

Shreyansh Jain
Git Series Sep 23, 2023 3 min read

Undoing Things in Git

One of the best things about Git is that almost nothing is truly permanent: there's a way to undo almost any mistake. This post walks through the most common "undo" scenarios, from a typo in your last commit message to rewriting your entire commit history.

1. Fixing the Last Commit


If you've just committed and realize you forgot to include a file, or made a typo in the commit message, you don't need a brand-new commit to fix it. Use --amend:
git commit --amend

This reopens your editor so you can update the commit message. If you only want to add the staged changes without touching the message, skip the editor with --no-edit:
git commit --amend --no-edit

Key rule: never amend a commit that's already been pushed. Amending doesn't edit the old commit: it creates a brand-new one with a new hash and replaces the old one. If anyone else has already pulled the original commit, your amended version will conflict with their history and cause confusion.

2. Unstaging a File


Before looking at how to undo changes, it helps to understand Git's three areas: the Working Directory (your local files), the Staging Area (files marked to be committed), and the Local Repository (committed history). The restore commands help you move files back through these stages.
!The Three Areas of Git: Working Directory, Staging Area, and Local Repository
Added something to the staging area by mistake? git restore --staged removes it from the staging area without touching your actual changes. They stay right where they are in your working directory:
git restore --staged <filename>

3. Discarding Changes to a File


If you want to throw away your edits to a file entirely and go back to the last committed version, use:
git restore <filename>

Be careful with this command: it permanently discards any uncommitted changes to that file. There is no way to recover them afterward.

4. Restoring a File from an Older Commit


Sometimes you don't want to discard a file's changes: you want to bring back an older version of it from a specific commit:
git checkout <commit-id> -- path/to/file

This pulls that file's contents from the given commit into your working directory, without affecting any other files or your commit history.

5. Undoing a Commit: revert vs. reset


This is where most of the confusion happens, so let's go through both options carefully.

git revert


git revert doesn't delete or replace anything. Instead, it creates a new commit that undoes the changes from a previous one. If a commit added a line, the revert commit removes that same line: your history moves forward, but the code ends up back where it was.
!git revert: a new commit D is appended to undo the changes of commit C
git revert <commit-id>

Because it adds to history instead of rewriting it, revert is the safer choice for undoing commits that have already been pushed or shared with others.

git reset


git reset takes a different approach: it moves your current branch pointer (HEAD) back to an earlier commit.
!git reset: the HEAD pointer moves backward from C to B, orphaning C
What happens to the changes in between depends on which mode you use:

  • git reset --soft : moves HEAD back, but keeps all the changes from the undone commits staged and ready to commit again. Use this when you want to combine or redo recent commits without losing any work.

  • git reset --mixed (the default): moves HEAD back and unstages the changes, but leaves them sitting in your working directory. This is the middle ground: nothing is staged, but nothing is lost either.

  • git reset --hard : moves HEAD back and throws away everything: staged changes and working directory changes alike. Use this with real caution, since the discarded changes are not recoverable.

  • git reset --keep : similar to --hard in that it discards the commits, but it tries to preserve any uncommitted local changes you currently have, and will abort if doing so would overwrite them. It's a safer alternative when you have uncommitted work you don't want to lose.


As a rule of thumb: reach for revert on shared branches, and save reset --hard for commits and branches that are still entirely local to you.

Enjoyed this essay?

Support my writing by buying me a coffee.