Undo changes in Git - Cheat sheet

Published 4/28/2019

This article is part of a series:


Git is one of these things that you learn progressively. You start with git add . to stage files, git commit -m “message” to commit them locally and finally git push to push them to the remote repository. But over time you make mistakes and if you always just google and paste in random commands, you might easily get confused by the sheer amount of commands like "git reset", "git revert", "git clean", "git checkout .", "git rebase -i", "git commit --amend" and more. Let’s go through them one by one!

Table of contents

Understanding key terms

You can always find more help here, but it is important to understand some key terms.

  • Index: When you stage files using git add it adds the files to an index file. So Staging is the same as index.
  • Untracked: If you create a new file, it is untracked until you stage it.
  • HEAD: HEAD points to the latest commit of your current branch. That's why you sometimes see the funnily chosen detached HEAD, when you check out an older commit for example.
  • The dot in git add . refers to the current directory. So all files in the current directory and in all sub directories will get staged. We will learn some more commands that make use of the dot.

Check out my e-book!

Learn to simplify day-to-day code and the balance between over- and under-engineering.

Don't undo: save changes for later use

git stash -u

This will save every untracked files (-u flag), staged and unstaged modifications.

To retrieve the latest stash again, run

git stash apply

To keep your stash list clean you can also execute git stash pop instead. It will do the same as apply, but also remove the applied stash from the stash list.

Use git stash list to retrieve a list of all your stashes.

Use case: You are working on something but suddenly need to change branches (e.g. to create a hotfix for a sudden urgent bug)

Bonus: To know what stash belongs to what, you can give your stash a note by running: git stash push -u -m "your message"

Don't undo: Add something to the latest commit or change the message

git commit --amend -m "added file and changed message to this"

amend allows you to add more files to the latest commit.

Use case: You forgot to stage a certain file that should have been part of the commit.

Bonus: Even if no file has been added you can still commit with the "amend" option to simply change the message.

If other people are working on your branch: Be careful to not amend when the latest commit has already been published (pushed). It would require git push --force

Unstage files (precommit)

git reset .

also often seen as just git reset

Example:

echo "code code code" >> index.js
git add .
git reset .

It will simply unstage index.js and put the changes back into your working tree. You can apply the --hard flag to completely get rid of your changes.

Use case: Out of habit, you staged all modifications using git add ., but want to unstage certain files to commit them separetely.

Bonus: To unstage only specific files you can do it the same way as with git add

git add User.js UserController.js UserService.js
git reset UserService.js User.js

This keeps UserController.js staged.

Revert changes (precommit)

git checkout .

git checkout is used to change branches, but if you check out a filepath instead, it has a different purpose.

If you have changed any files locally, this will revert your changes with either what is in the index or in the commit. More on that in a moment.

Example:

echo -n "1" >> newfile
git add .
git commit -m "added newfile"

echo -n "2" >> newfile

So we created a new file, staged and commited it and then appended the text "2" to the file.

If you now run git checkout newfile it will remove any local changes, in this case "2". Your working tree will be clean again.

Let's look at what happens when you start staging in between. This will give you a new perspective into git add, making the command more powerful than ever.

echo -n "1" >> newfile
git add .
git commit -m "added newfile"

echo -n "2" >> newfile
git add .

echo -n "3" >> newfile

This is the content of the file in the different states

  • HEAD: "1"
  • Index: "12"
  • Working Tree: "123"

If you now run git checkout newfile it will only remove the content 3 from your local changes. In other words, if it finds the file in the index, it will revert the changes by what is in the index, not in the HEAD. It will still have newfile staged with the content "12".

To also remove "2" you have to first unstage the file as we learned and then check it out again.

git reset
git checkout .

Since it only replaces the files with those from the index / HEAD, git checkout won't do anything with untracked files.

Use case: You made modifications to file A and when modifying file B you realized the changes in file A were actually not necessary and it's better to just check it out again.

Bonus: If a file you want to checkout is unfortunate enough to have the same name as a branch, you have to checkout like this git checkout -- master to avoid checking out the master branch for example.

Remove new/untracked files and directories

git clean -f

Similar to git checkout . with the difference that it only works for untracked files. You can run git clean --dry-run or git clean -n to see which files would be permanently deleted. Add the "-d" flag to include directories.

touch newfile
git clean -d -n

The output will be "Would remove newfile".

Bonus: Use git clean -i to start interactive cleaning, giving you more options over what to do with each file individually.

Revert a commit in a new commit

git revert commit-id

Reverts the changes of the commit ID and creates a new commit for it.

Use case: A commit that has been pushed causes a bug and has to be reverted.

Bonus: Apply the -e or --edit flag to modify the commit message. For example, you can add the reason why this commit has to be reverted.

Remove latest commit(s) without a new commit

git reset HEAD^

Imagine you commit something by accident and you want to undo the commit. git reset HEAD^ will revert the latest commit like it never happened and puts the modifications back to your local working tree. It will not create a new commit and the latest commit will disappear from the history.

We saw git reset before already to unstage files. When you pass a commit however, you can actually reset HEAD to whatever commit you want. Imagine you have the commits A, B and C. With git reset A A will become the latest commit and if you git log, you will no longer find B and C.

We said that git reset HEAD^ keeps the changes in the working tree. So in other words, it resets the HEAD and the index.

Use the "--soft" flag to only reset the HEAD, which means that the changes will remain "staged". Use the "--hard" flag to reset HEAD, index and the working tree, which means the changes will be completely deleted.

Instead of git reset HEAD^ you can also write git reset HEAD^1, git reset HEAD~1 or git reset HEAD~.

If other people are working on your branch: Be careful to not reset HEAD to a previous commit when the commits you reset have already been published. It would require git push --force

Use case: You committed modifications locally, but realized you were comitting to the wrong branch.

Bonus: There are a total of four ways to reset more than just the latest commit.

All of these four lines reset the last two commits

git reset HEAD^2
git reset HEAD^^

git reset HEAD~2
git reset HEAD~~

Bonus2: git reset does not actually remove the latest commit, it simply "rolls back" HEAD to the commit you want. B and C are still saved for 30 days.

Change history

Let's take a quick look into interactive rebasing. We learned git reset HEAD~1 --hard as a way to remove the latest commit. We can achieve the same thing with interactive rebasing, just that it is more powerful. rebase this time actually changes the commit object and doesn't just point HEAD to a specific commit.

git rebase -i HEAD~4

We see the same HEAD~4 as with git reset before. 4 refers to the number of commits you want to rebase.

This will now open an editor (vim?) with the following content

pick 123a44b0 your latest commit
pick C23a44b0 commit 3
pick B23a44b0 commit 2
pick A23a44b0 commit 1

# Rebase ...
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit

Let's replace

pick 123a44b0 your latest commit
pick C23a44b0 commit 3
pick B23a44b0 commit 2
pick A23a44b0 commit 1

by

pick 123a44b0 your latest commit
fixup C23a44b0 commit 3
f B23a44b0 commit 2
drop A23a44b0 commit 1

As you can see, you can either write out the option like fixup or use the abbreviation f in this case.

This will squash (merge) "C23a44b0" and "B23a44b0" into "123a44b0", making one commit out of it. Regarding "A23a44b0", this commit will get completely removed. So once the rebase is complete you will end up with only one commit "123a44b0".

If other people are working on your branch: Be careful to not rebase when the commits you want to rebase have already been published. It would require git push --force

Use case: You are working all alone on your own branch and want to have a clean list of commits for the PR/MR.

Bonus: You can even move commits around by just changing the order they appear in the list.

Remove branch locally

git checkout master

git branch -D branch-to-delete

Use case: Instead of reverting a bunch of commits, sometimes it is just faster to delete your local branch and check it out again.

Remove remote branch

git push origin branch-to-delete --delete

Use case: After pushing you realize the name you have chosen doesn't make sense and you want to change it (Bonus: Do so with git branch -m "new-name")


Conclusion

Git is quite a beast to master and I am sure there are other ways to achieve some of these tasks. Please leave a comment if you know any and I can add them to the list.

Want to learn more about Git or a way to remember all these small things. Why not rebuild something like VS Code's Git integration. Here is the source code for starters: https://github.com/Microsoft/vscode/tree/master/extensions/git.