Undoing mistakes with Git
One of Git’s powers is being able to “go back in time” to a previous version to undo a terrible mistake or simply to start fresh.
How to identify the scenario that applies to you
We will walk through some common scenarios where you might want to undo your work and reset to a known safe state.
“Going back in time” depends on what you want to change and the current state of your repository in terms of (a) what’s changed in the workspace, (b) what is staged in the index, and (c) what has been committed to the local repository.
Use the git status
command to identify staged and unstaged changes, and git log
to check the local repo version history.
Starting state
In Lab: Git Basics, we created a Git repository for a simple speakeasy/
project. We added two files, main.py
and README.md
, and committed two versions:
We will pick up our example from this point.
Oops #1: Deleted something from the workspace
- Open Visual Studio Code for the
speakeasy/
folder. - Now delete
main.py
Let’s say you want to recover what you just deleted. This scenario may involve one file, many files, directories, or anything in the project folder. So when I use the word “file” below, I mean any of those things.
Your options depend on whether the file has been staged with git add
or committed at some point in the past.
If the file has been staged before
- First try using your IDE’s undo feature: CTRL+Z or CMD+Z. If you see the file reappear, you are good to go.
- If undo doesn’t work, use
git restore [name]
. Git will place a copy in the workspace.
If the file has not been staged
- Try using your IDE’s undo feature.
- If that doesn’t work, check your operating system’s “trash can”.
- Sorry. It’s gone.
Oops #2: Undoing unstaged changes
Suppose you’re editing a file tracked by Git. You don’t like what you’ve done, and want to start over from most recent version.
- Make sure
main.py
is back in your workspace. - Add the following code to
main.py
:import random def silly_compliment(): compliments = [ "You're as useful as a screen door on a submarine, but twice as fun!", "Your brain is like a sponge... except it soaks up memes more than facts!", "You're as rare as a unicorn at a hotdog stand." ] return random.choice(compliments)
- Save the file.
- Add the line
I like working on it!
toREADME.md
and save the file. - Make a new file
hello.py
and addprint("Hello world!")
to it. - Run
git status
git status
tells you that main.py
and README.md
have been modified but are not staged, and it tells you that blah.py
is new and untracked:
Our changes are only in the workspace, they are not staged in the index yet.
Now, let’s undo some changes:
- Run the command
git checkout -- main.py
to reset to the file to the most recent version, in this case, the versionb424cc
.- The contents of
main.py
will change in the editor. - Notice that
hello.py
andREADME.md
are unchanged. This is because we specifiedmain.py
as the target ofgit checkout --
- The contents of
- Restore the changes to
main.py
by undoing with CTRL+Z or CMD+Z. - Now run the command
git checkout -- .
- Notice that both
main.py
andREADME.md
reset to their previous version. This is because we specified the target.
, which is shortcut for “the current working directory”. Bothmain.py
andREADME.md
are tracked by Git, so they both reset. - However,
hello.py
is untracked by Git so it is unaffected.
- Notice that both
After running these commands, we are in the state below where hello.py
is a new file but not being tracked by Git. Both README.md
and main.py
are as they were in the most recent committed version.
Now what if you want to get rid of an untracked, unstaged file like hello.py
? Just delete the file!
The checkout --
command replaces the workspace files with the most-recently-committed versions of those files in the local repository, i.e., the files as they were in b424cc
.
Oops #3: Undoing staged changes
Suppose you are adding, editing, or deleting files and you have run the git add .
command to stage the changes in the index. You realize that you made a mistake, and you do not want to save those changes. You either want to work on them some more, or you simply want to start over.
We will start at the end of the previous scenario: main.py
and README.md
are unchanged and look like they do in the most recent version b424cc
, while we added added a new file hello.py
that is not staged yet.
Run the following:
- Re-add the following code to
main.py
:import random def silly_compliment(): compliments = [ "You're as useful as a screen door on a submarine, but twice as fun!", "Your brain is like a sponge... except it soaks up memes more than facts!", "You're as rare as a unicorn at a hotdog stand." ] return random.choice(compliments)
- Run
git add .
to stage the changes to bothmain.py
and the newhello.py
file. - Run
git status
main.py
and hello.py
are now in the index of changes we want to save to a new version, but we haven’t committed that new version to the local repository yet.
Suppose at this point that we need to do more work in hello.py
and main.py
. Maybe we’ve made a mistake, and we’re not ready record these changes.
- Run the command
git reset hello.py
. This will unstage the file, meaning it will not be included in the commit until you rungit add
again. - You can also run
git reset .
to unstage any staged changes. The files will be unchanged in your working directory.
The files still have all their changes in the workspace. You are ready to edit and fix up whatever you need.
Oops #4: Completely restart from the last version
This is a common scenario. You work for a bit and then decide that all the changes you have made are bad, and the easiest thing is just to start over.
You want to wipe out all the changes in both your workspace and the index. Be careful: once you do this, you can’t undo it.
Let’s start where we ended in the previous figure: we’ve changed main.py
and added the new file hello.py
. These changes are not staged in the index yet.
Do the following:
- Run
git status
to see that we have unstaged and uncommitted changes. - The
git reset --hard HEAD
HEAD
is a special reference that means “the most recent committed version”.--hard
argument tells Git “destroy changes to tracked files in the workspace and the index”
You should see output like
HEAD is now at b424cc4 Added message and README file
b424cc4
is the most recent committed version in the local repository, and “Added message and README file” was the message for that version.
Run git status
:.
Notice that untracked files are unaffected. We have not added or committed hello.py
, so it remains untouched. But main.py
has been reset to its most recent version.
All together, git reset --hard HEAD
says “reset the tracked files in the workspace by replacing (--hard
) the workspace contents with the most recent version (HEAD
)”
Again, this is a destructive action. You cannot undo it once done. But, it is very useful for starting fresh. Your local repository is unaffected by the command.
Oops #5: Undoing the most recent commit
You have run git add .
and then a git commit -m "<message>"
. Committing saves a new version to the local repository.
Maybe you are unhappy with the version and you want to edit your work. Maybe you forgot to add a file that needed to be there. In these cases, the simplest thing is often to make the changes and just make another commit.
You committed version should be “good code”. Bug free, compiles, works. However, sometimes you commit a mistake. You find a terrible bug in your code. Or you committed a syntax error and didn’t notice. These scenarios call for you to undo the commit.
Starting from the previous scenario, we have hello.py
in the workspace but untracked. Let’s introduce a bug to main.py
:
- Open
main.py
and add the linetip = float(input("Enter a tip amount: "))
- Make sure to save
main.py
- Run
git add .
- Run
git commit -m "Enable user to type a tip amount"
You will see output like:
[main 81a55e5] Enable user to type a tip amount
2 files changed, 3 insertions(+)
create mode 100644 hello.py
We should now have three versions in our local repository. Run git log
to see them:
We realize that we have committed a bug. tip = float(input("Enter a tip amount: "))
will crash the program if the user types in a non-numeric number for the tip, like "one dollar"
. We want to undo the commit so we can fix the bug and to keep our version history containing only “good code”.
You have two options here:
- You may have some changes to your workspace that you want to keep. Like you want to keep
hello.py
. Or maybe your code inmain.py
is pretty good, and you just want to fix it up a little bit. - Your last commit was a total disaster. You don’t want to keep any changes you made to
main.py
orhello.py
. You want to completely throw away the most recent version and go back to the one before it.
Option 1: Preserve your work, fix it, then make a new commit.
Run the command git reset HEAD~1
. You will see output like:
Unstaged changes after reset:
M main.py
Now run git log
. You will see something like:
commit b424cc472f7276dc35493abbd186563a191ca25b (HEAD -> main)
Author: Lucas Layman <laymanl@uncw.edu>
Date: Mon Oct 21 15:21:44 2024 -0400
Added message and README file
commit 8356ea035b8d6538f9ea4eabe2393d6cd6016553
Author: Lucas Layman <laymanl@uncw.edu>
Date: Mon Oct 21 15:13:00 2024 -0400
First commit of main.py
Notice that git log
only shows two versions! What have we done? Your current Git state is like this:
The command git reset HEAD~1
tells the local repository to “forget” the most recent version. It’s like it never happened.
However, the files in your workspace and index are unchanged! All the edits and additions are still there for you to work with, they are just not committed.
Now you have the opportunity to fix up those files, add
them, and commit
them.
Option 2: Disaster! Delete the last version and reset all the files
This is just like Oops #4 where you reset the tracked files, but you also want to destroy the most recent commit.
The command to do this is git reset --hard HEAD~1
. This command is destructive and you cannot undo the consequences.
Assuming you have changes to main.py
and hello.py
from the previous scenario:
- Do
git add .
andgit commit -m "Enabling the user to enter a tip"
to stage and commit a new version - Run
git reset --hard HEAD~1
- Run
git log
to see the version history
hello.py
is unaffected because it is untracked, however, main.py
and README.md
are reset to their version 2 status. We’ve also deleted the bad version.
Recap
Git has even more functionality for “going back in time”, such as going back two, three, or more versions in the past. Or undoing multiple commits at once. Those use cases can be tricky to do correctly without unintended consequences.
For now, the “Oops” scenarios above will be sufficient 95% of the time as you develop your Git skills:
- Deleted a file from the workspace: Undo (CTRL+Z/CMD+Z) or
git restore <filename>
- Undoing unstaged (not
add
) changes:git checkout -- <filename>
- Undoing staged (
add
ed) changes:git reset <filename>
- Completely restart from the last version:
git reset --hard HEAD
. This is destructive! - Undoing the most recent commit:
- and keep your work:
git reset HEAD~1
- and throw away work:
git reset --hard HEAD~1
. This is destructive!
- and keep your work:
Knowledge check
- (Question) Describe how
git status
andgit log
help identify a repository’s state. - (Question) What command would you use to recover a deleted file that was previously staged or committed?
- (Question) How does
git checkout -- <file>
differ fromgit restore?
- (Question) Explain how to undo changes that are staged but not committed.
- (Question) What happens to untracked files when you run
git checkout -- .
? - (Question) Which command do you run to completely reset your working directory to the most recent version?
- (Question) Which command do you run to destroy/remove the last version in the local repository?
- (Challenge) Simulate deleting a file and use Git commands to recover it.
- (Challenge) Experiment with staging changes, then undo them.