Skip to content

Git - Fix Mistakes

Everyone makes mistakes. Git provides several ways to undo or revert changes, depending on whether the changes are in your working directory, staging area, or have already been committed.

Discarding Local Changes (Working Directory)

Section titled “Discarding Local Changes (Working Directory)”

If you’ve made changes to a file in your working directory that you haven’t staged yet, and you want to discard them (revert the file to how it was at the last commit/HEAD), you can use git restore <file>.

Let’s say Jerry accidentally modified string_operations.c:

[jerry@dev-machine src]$ pwd
/home/jerry/project/src
[jerry@dev-machine src]$ # ...Jerry makes some unwanted changes to string_operations.c...
[jerry@dev-machine src]$ git status -s
M string_operations.c
[jerry@dev-machine src]$ git restore string_operations.c
[jerry@dev-machine src]$ git status -s
# No output, meaning working directory is clean

The changes Jerry made to string_operations.c are now gone, and the file is restored to its last committed state. Before git restore was introduced, git checkout -- <file> was used for this.

Similarly, if you accidentally deleted a file, git restore <file> can bring it back:

[tom@dev-machine src]$ ls
Makefile string_operations.c
[tom@dev-machine src]$ rm string_operations.c
[tom@dev-machine src]$ git status -s
D string_operations.c
[tom@dev-machine src]$ git restore string_operations.c
[tom@dev-machine src]$ ls
Makefile string_operations.c
[tom@dev-machine src]$ git status -s
# Working tree clean

Unstaging Files (Removing Changes from Staging Area)

Section titled “Unstaging Files (Removing Changes from Staging Area)”

If you’ve staged a file with git add and want to unstage it (remove it from the staging area but keep the changes in your working directory), use git restore --staged <file>.

Tom modifies string_operations.c and stages it:

[tom@dev-machine src]$ # ...Tom modifies string_operations.c...
[tom@dev-machine src]$ git add string_operations.c
[tom@dev-machine src]$ git status -s
M string_operations.c

Now, Tom decides he doesn’t want to commit these changes yet, so he unstages the file:

[tom@dev-machine src]$ git restore --staged string_operations.c
[tom@dev-machine src]$ git status -s
M string_operations.c

The file is no longer staged (note the ‘M’ is now in the first column, not second), but the modifications are still present in the working directory. Before git restore --staged, git reset HEAD <file> was commonly used for this.

git reset is a powerful command used to undo committed changes by moving the current branch’s HEAD pointer to a previous commit and optionally modifying the staging area and working directory. It’s crucial to understand its different modes. Caution: git reset can lead to lost work if used improperly, especially with --hard.

Imagine the commit history is C1 <- C2 <- C3 (HEAD -> main). We want to reset to C2.

Moves HEAD to <commit-SHA>. The staging area and working directory are not changed. Changes from commits after <commit-SHA> (in our example, C3’s changes) will appear as staged changes.

# Assuming HEAD is at commit C3 (e.g., 5776472) and C2 is 29af9d4
[jerry@dev-machine project]$ git log -1 --oneline
5776472 (HEAD -> main) Removed executable binary
[jerry@dev-machine project]$ git reset --soft 29af9d4
[jerry@dev-machine project]$ git log -1 --oneline
29af9d4 (HEAD -> main) Added compiled binary
[jerry@dev-machine project]$ git status
# Changes from commit 5776472 will be staged

This is useful if you want to re-commit the changes from C3 differently, perhaps combining them into a new commit or splitting them.

Moves HEAD to <commit-SHA> and updates the staging area to match <commit-SHA>. The working directory is not changed. Changes from commits after <commit-SHA> will appear as unstaged local modifications in your working directory.

[jerry@dev-machine project]$ git reset --mixed 29af9d4
# Or simply: git reset 29af9d4 (since --mixed is default)
[jerry@dev-machine project]$ git log -1 --oneline
29af9d4 (HEAD -> main) Added compiled binary
[jerry@dev-machine project]$ git status
# Changes from commit 5776472 will be in the working directory, unstaged.

This is useful to completely un-commit changes but keep them locally to rework.

Warning: This command can lead to permanent loss of uncommitted changes and commits after <commit-SHA>. Use with extreme caution.

Moves HEAD to <commit-SHA>, updates the staging area AND the working directory to match <commit-SHA>. Any changes in commits after <commit-SHA>, and any uncommitted local changes (staged or unstaged), will be permanently deleted.

Jerry has made some local modifications to string_operations.c and even staged them. The latest commit is 5776472.

[jerry@dev-machine src]$ head -2 string_operations.c
// Temporary debug line
#include <stdio.h>
[jerry@dev-machine src]$ git add string_operations.c
[jerry@dev-machine src]$ git status -s
M string_operations.c
[jerry@dev-machine src]$ git reset --hard 5776472 # Reset to the last commit
HEAD is now at 5776472 Removed executable binary
[jerry@dev-machine src]$ git status -s
# Working tree clean
[jerry@dev-machine src]$ head -2 string_operations.c
#include <stdio.h>
// The temporary debug line is gone.

The --hard reset discarded all staged and unstaged local changes, reverting string_operations.c to its state in commit 5776472. If he had reset to an earlier commit, commits after that point would also be gone from the branch history.

If you want to undo a commit that has already been pushed to a shared repository, git reset is dangerous as it rewrites history. Instead, use git revert <commit-SHA>. This creates a new commit that applies the inverse of the changes introduced by the specified commit. This preserves history and is safe for shared branches.

[jerry@dev-machine project]$ git revert HEAD # Reverts the last commit
# This will open an editor for the new revert commit message.
# After saving, a new commit is created that undoes the changes of HEAD.

git revert is the recommended way to undo changes on public/shared branches.