Let’s Pretend This Never Happened – Undoing Changes in Git

Any beginner developer has been there – you’re working on a project, creating branches, making commits, merging with master, and oops – you made a mistake. You have a ton of commits and the code broke somewhere. Someone else merged your branch and now it’s four steps ahead with incorrect code. You made all these great changes locally but realized you never created a separate branch and were working off of the master. Mild panic sets in.

But guys, it’s cool. The ability to ‘undo’ or rollback mistakes is a critical feature of any version control system. Anything that is committed in Git can almost always be recovered. When you make a commit, Git takes a snapshot of the current state and records changes to the repository. It’s best practice to ‘commit often and perfect later.’ Frequent commits can help you pinpoint when and where something broke.

Undoing changes in Git centers around three commands: git checkout, git reset, and git revert. I thought the diagram below was helpful for understanding these commands. It looks at a Git repository as three components: a working directory, the staged snapshot (or index), and a commit history.

Before undoing changes, use git status, git log, or git reflog to check the state of your repository, commit history, etc.

git log shows the current HEAD and its ancestry by recursively looking up each commit’s parent.

git reflog (for Reference Logs) is an ordered list of commits to which the HEAD has pointed. It’s a useful tool for for recovering your project’s history.

HEAD in Git is a symbolic reference to your current branch and the last known state of your working directory (which is usually the last commit). Think: “what is my repo currently pointing at?”. It’s pointing at the current branch reference, which in turn points to the last commit you made on that branch.

git checkout

When to use: you made a branch to play around in, the file is a hot mess and you want nothing to do with it. You don’t like the code in a certain file but you’ve made changes in other parts of the program that you’d like to commit. You’d like to review the code in the last commit.

git checkout <filename> discards the changes in your working directory and alters the file to its previous commit. If you haven’t committed, any changes you made to that file are gone. This is a destructive command so only use it if you’re absolutely sure that you don’t want to keep these changes. This isn’t just a ‘let’s pretend this never happened’ scenario, it’s a ‘burn all the evidence’ solution. Use git diff to find out exactly what you will be throwing away.

git checkout <commit> updates all the files in the working directory to match that commit. For example, git checkout HEAD~2 would checkout two commits back on the current branch. NOTE that checking out a commit puts you in a detached HEAD state (this is a thing) since it doesn’t point to a particular branch. If you start making changes and committing in the detached head state, all those changes will be lost once you switch to another branch.

git revert

When to use: You’re trying to fix a bug and find that it was introduced in a single commit. You are working on a public branch. Any time you don’t want to rewrite the commit history.

git revert undoes a commit by creating a new commit. Note that git it doesn’t restore the project to a previous state by removing all subsequent commits, it just undoes a previous commit. For example, git revert HEAD~2 will find the changes to the second to last commit, undo those changes, then make a new commit with the current changes. Since git revert doesn’t alter a repo’s commit history, it’s considered the safest option for undoing changes.

git reset

When to use: You haven’t shared the changes with anybody (everything is local). You’ve committed your code to a certain branch but want to undo the most recent commits. You just started working on a project and want to start over.

git reset takes the current branch and resets it to somewhere else, with options to move the index and working tree with it. On the commit level, reset moves the tip of the branch to another commit. For example, git reset HEAD~2 – moves the branch back by two commits, which will then be thrown away.

There are different level of git reset, which are represented by the following flags:

–soft: the staged snapshot and working directory are not altered. All your files stay intact and git status will return ‘changes to be committed’. Use this flag when the work is good and you just want to commit differently.

–mixed: resets the snapshot to match the specified commit, but the working directory is unchanged (this is the default). All your files are intact, but any differences between the original commit and the one you reset to will show up as local modifications (or untracked files). Use when you’ve made some bad commits that you want to fix up and recommit. You’ll have to add these files back to the staging area.

–hard: everything matches the commit you specified and any local changes not committed are obliterated. Use when things have gone horribly wrong and you need to work with a clean slate (another ‘burn all evidence’ situation). This is a permanent ‘undo’.