undo in git

Git is one of the most known, and controversially, most complicated tools for managing source code.

There are a lot of tutorials showing how to use git to add, commit, and pull your code, view the history, change between branches, and so on. So far all good, since git is complicated: every file has multiple possible states, like committed, modified, and staged. Moreover, you may have to consider submodules, branches, merges, the stash, etc.

The fact that it is so complicated also means that for every action, there are multiple chances to make errors. You may forget to add some files, commit some unwanted ones, make a typo when commenting etc. Depending on your repository’s state, there are different ways to undo your error. Of course, before trying to undo something, you should at least have an idea how you normally do something with git ;-).

A test repository

Through documentation, articles, forums, stack overflow, and other resources, you might find how to undo your actions. I’m grouping in this post the most common situations I face on a daily basis when working with git, in order to have a quick reference with a minimal example.

Let’s create an empty repository and add a "Hello World!" text file. All commands typed in the terminal are preceded by a >, my current prompt. Commands that begin with # are comments, intended to clarify what is happening.

> git init
Initialized empty Git repository in /home/fekir/test/.git/
> echo "Hello World" > hello.txt
> git add hello.txt
> git commit -m "added hello.txt"

[master (root-commit) 5b50892] added hello.txt
 1 file changed, 1 insertion(+)
 create mode 100644 hello.txt
> git log
commit 5b50892d9dbd442b3d3469fe0f2301ff69922308
Author: fekir <fekir@example.com>
Date:   Sun Feb 30 25:00:00 2017 +0000

    added hello.txt

Delete local modifications

Let’s say we have edited a local file, and after a while, we realize it would just be easier to undo all local changes. In this case, we can simply use the git checkout command to undo everything.

> echo "Hello, World" > hello.txt
> git status
On branch master
Changes not staged for commit:
	modified:   hello.txt

no changes added to commit
> git diff hello.txt
diff --git a/hello.txt b/hello.txt
index 557db03..3fa0d4b 100644
--- a/hello.txt
+++ b/hello.txt
@@ -1 +1 @@
-Hello World
+Hello, World
> # undo changes to hello.txt
> git checkout -- hello.txt
> git status
On branch master
nothing to commit, working tree clean

You should always use the double minus, --, since the checkout command also accepts branch names as parameters. In case you have a branch with the same name as your file, you will not undo your changes, but change branch. To disambiguate between branch names and file names, you should use -- before the name.

Save local modifications for later

Like in the example above, we have done some local unnecessary changes, but we do not want to lose them, since we know that we will need the later. Git checkout simply deletes our working space, we can use git stash to save it somewhere else.

> echo "Hello, World" > hello.txt
> git status
On branch master
Changes not staged for commit:
	modified:   hello.txt

no changes added to commit
> git stash
Saved working directory and index state WIP on master: 68ef0a6 added hello.txt
HEAD is now at 68ef0a6 added hello.txt
> # verify that our stash is not empty
> git stash list
stash@{0}: WIP on master: 68ef0a6 added hello.txt

When we want to apply those changes, we will simply use git stash pop or git stash apply, modify our changes accordingly, make our commit, and lastly delete the stash.

> git status
On branch master
nothing to commit, working tree clean
> git stash apply
On branch master
Changes not staged for commit:
	modified:   hello.txt

no changes added to commit
> git status
On branch master
Changes not staged for commit:
	modified:   hello.txt

no changes added to commit
> git diff hello.txt
diff --git a/hello.txt b/hello.txt
index 557db03..3fa0d4b 100644
--- a/hello.txt
+++ b/hello.txt
@@ -1 +1 @@
-Hello World
+Hello, World

Of course we can stash multiple changes. In that case, we need to specify which stash we want to apply. In our previous example it would have been the first stash: git stash apply stash@{0}

And we can also attach a message to our new stash:

> echo "Hello, World" > hello.txt
> git status
On branch master
Changes not staged for commit:
	modified:   hello.txt

no changes added to commit
> git stash save "descriptive message"
Saved working directory and index state On master: descriptive message
HEAD is now at 68ef0a6 added hello.txt
> git st
On branch master
nothing to commit, working tree clean
> git stash list
stash@{0}: On master: descriptive message

After having applied our stash back to our working area, if we do not need those changes anymore, we can delete them (happens automatically when using git stash pop)

> git stash list
stash@{0}: On master: descriptive message
> git stash drop stash@{0}
Dropped stash@{0} (1500f0a43ad6a0f7db2e465515645d46c8cb96b7)
> git stash list
>

Undoing add (aka unstage file)

After having typed git add filename, before committing, you will notice that you have added an extra file. What we want to do now is unstage it, without losing the changes we have made. In this case, we should use git reset. For example:

> echo "Hello, World" > hello.txt
> echo "# Sample readme" > readme.txt
> git add readme.txt
> git add hello.txt
> git status
On branch master
Changes to be committed:
	modified:   hello.txt
	new file:   readme.txt

> # undo add of hello.txt
> git reset hello.txt
Unstaged changes after reset:
M	hello.txt
> git status
On branch master
Changes to be committed:
	new file:   readme.txt

Changes not staged for commit:
	modified:   hello.txt

Now it’s like you just added the readme.txt file. Typing git reset without a filename would have unstaged all files. Notice that all changes in our hello.txt file are intact.

If you have added a file and realize later that you have forgotten something before committing, you don’t need to unstage your file. You can simply edit it and add it again. Here is how.

> echo "Hello, World" > hello.txt
> git add hello.txt
> # add some content after first git add
> echo "Today we will..." >> hello.txt
> git status
On branch master
Changes to be committed:
	modified:   hello.txt

Changes not staged for commit:
	modified:   hello.txt

> # add already staged file a second time and verify that all changes are effectively saved
> git add hello.txt
> git status
On branch master
Changes to be committed:
	modified:   hello.txt

> # verify that all changes are effectively saved
> git diff --cached hello.txt
diff --git a/hello.txt b/hello.txt
index 557db03..3636e86 100644
--- a/hello.txt
+++ b/hello.txt
@@ -1 +1,2 @@
-Hello World
+Hello, World
+Today we will...

Undoing the last commit

Sometimes we do notice some errors not only after adding the files but after committing them. Luckily we can still fix our errors with git reset. Let’s suppose that we did not actually want to add a readme.txt file, but a readme.md file. We could make a second commit, but we can fix our error without it being recorded in the history.

> echo "Sample readme" > readme.txt
> git add readme.txt
> git commit -m "add readme"
[master e773a2e] add readme
 1 file changed, 1 insertion(+)
 create mode 100644 readme.txt
> git log
commit e773a2e6b47fc9b3b45f603d4142a3aff037dd16
Author: fekir <fekir@example.com>
Date:   Sun Feb 30 25:00:00 2017 +1400

    add readme

commit 5b50892d9dbd442b3d3469fe0f2301ff69922308
Author: fekir <fekir@example.com>
Date:   Sun Feb 30 25:00:00 2017 +1400

    added hello.txt
> # remove the last commit
> git reset HEAD~1
> git log
commit 5b50892d9dbd442b3d3469fe0f2301ff69922308
Author: fekir <fekir@example.com>
Date:   Sun Feb 30 25:00:00 2017 +1400

    added hello.txt
> # verify that changes are not lost (in the working area)
> git status
On branch master
Untracked files:
	readme.txt

nothing added to commit but untracked files present
> cat readme.txt
Sample readme

git reset HEAD~1 will bring us at the last status (HEAD), minus 1. As shown, all our changes are saved in the current workspace.

Undoing the last commit (soft)

Another possibility is to use git reset with the --soft parameter, in order to leave the commit changes in the staged area, instead of our workspace

> echo "# Sample readme" > readme.txt
> git add readme.txt
> git commit -m "add readme"
[master ea54401] add readme
 1 file changed, 1 insertion(+)
 create mode 100644 readme.txt
> git log
commit ea544011526600d364a68e2afd9e1bea021d91a3
Author: fekir <fekir@example.com>
Date:   Sun Feb 30 25:00:00 2017 +1400

    add readme

commit 5b50892d9dbd442b3d3469fe0f2301ff69922308
Author: fekir <fekir@example.com>
Date:   Sun Feb 30 25:00:00 2017 +1400

    added hello.txt
> # remove the last commit (soft)
> git reset --soft HEAD~1
> git log
commit 5b50892d9dbd442b3d3469fe0f2301ff69922308
Author: fekir <fekir@example.com>
Date:   Sun Feb 30 25:00:00 2017 +1400

    added hello.txt
> # verify that changes are not lost (in the staging area)
> git status
On branch master
Changes to be committed:
	new file:   readme.txt

Therefore calling git reset --soft HEAD~1 and git reset is the same as calling git reset HEAD~1. If your working directory is dirty, it may be helpful to distinguish the files that were part of the last commit from the others, otherwise git reset without --soft might be sufficient for your needs.

Fix last commit message

This error happens (at least to me), pretty often. Luckily it is also easy to fix. Just use git commit --amend after your last commit.

> echo "Hello, World" > hello.txt
> git add hello.txt
> git commit -m "my grat message"
[master 3d9552f] my grat message
 1 file changed, 1 insertion(+), 1 deletion(-)
> # fix typo
> git commit --amend -m "my great message"
[master af2bc9c] my great message
 Date: Sun Feb 30 25:00:00 2017 +1400
 1 file changed, 1 insertion(+), 1 deletion(-)
> git log
commit af2bc9cbcb8d223e3c91618f78d59c6124bc5ee3
Author: fekir <fekir@example.com>
Date:   Sun Feb 30 25:00:00 2017 +1400

    my great message

commit 5b50892d9dbd442b3d3469fe0f2301ff69922308
Author: fekir <fekir@example.com>
Date:   Sun Feb 30 25:00:00 2017 +1400

    added hello.txt

git commit --amend, without the -m parameter, will open your text editor allowing you to edit your commit message, similarly to git commit. If you look at the history, you will notice that there is no sign of your previous typo.

WARNING: Do not stage any file, before calling git commit --amend, in case you do not want to commit these changes as well (see next paragraph).

Update last commit content

git commit --amend is more powerful than one might think. It actually reverts your committed changes to the staging area and, after your edits, commits them back. Therefore, if we notice that we need to do some minor changes to a file, we can still do these after our commit.

> echo "Hello beautiful World" > hello.txt
> git add hello.txt
> git commit -m "update hello"
[master 22b5b96] update hello
 1 file changed, 1 insertion(+), 1 deletion(-)
> # add missing edit for last commit
> echo "It's a wonderful day" >> hello.txt
> git add hello.txt
> git commit --amend --no-edit
[master f0a456b] update hello
 Date: Sun Feb 30 25:00:00 2017 +1400
 1 file changed, 2 insertions(+), 1 deletion(-)
> # verify last commit contains all our modifications
> git show HEAD
commit f0a456b7e04ed75e26c3ec814337b109a9824574
Author: fekir <fekir@example.com>
Date:   Sun Feb 30 25:00:00 2017 +1400

    update hello

diff --git a/hello.txt b/hello.txt
index 557db03..63d68a9 100644
--- a/hello.txt
+++ b/hello.txt
@@ -1 +1,2 @@
-Hello World
+Hello beautiful World
+It's a wonderful day

In this example, I was not interested in modifying the commit message, therefore I used the -no-edit parameter.

Fix older commit message

For changing an older commit message, we must use git rebase.

Let’s add a couple of commits to our repository, since changing the last commit message is done with git commit --amend.

> echo "Hello, World" > hello.txt
> git add hello.txt
> git commit -m "updated hello.txt"
> echo "Goodbye World" > hello.txt
> git commit -m "Update hello.txt again"

Now we want to change the message updated hello.txt to Update hello.txt. Therefore we need to go two commits back.

> git rebase -i HEAD~2

An editor will open, and ask us what we want to do with all commits between HEAD and HEAD~2. The default selection is simply to pick every single commit.

pick 10f2ac2 updated hello.txt
pick 2f8c008 Update hello.txt again

Since we want to change the commit message of 10f2ac2, we will change it to

edit 10f2ac2 updated hello.txt
pick 2f8c008 Update hello.txt again

save the changes and close the editor. After closing the file, git will bring us in a state right after we have committed 10f2ac2. Now we can use git commit --amend to change the commit message, as shown above. After changing our commit message, we can tell git to go forward to the next edit, or to finish the rebase, if there was nothing else to change.

> git commit --amend -m "Update hello.txt"
> git rebase --continue
Successfully rebased and updated refs/heads/master.
> # verify that old commit message did really change from "updated" to "Update"
> git log
commit f3258aa4a18f34e0b0fc7ff271460f60b58d1740
Author: fekir <fekir@example.com>
Date:   Sun Feb 30 25:00:00 2017 +1400

    Update hello.txt again

commit 4c0db1713b48a5eab2238a75aa6bbd29a969f53c
Author: fekir <fekir@example.com>
Date:   Sun Feb 30 25:00:00 2017 +1400

    Update hello.txt

commit 68ef0a66758a79a74ec24587baf496ac516e2762
Author: fekir <fekir@example.com>
Date:   Sun Feb 30 25:00:00 2017 +1400

    added hello.txt

You will not be able to change your first commit this way, since you need to give as parameter the commit before the first commit you want to edit. If you type git rebase -i HEAD~3 git will give a fatal: Needed a single revision error. Fortunately, in this case, we can use git rebase -i --root to solve this problem.

Rebasing

With rebase we can split, reorder and do a lot of other things. But this would not be a simple "undo"-guide anymore, but the begin of a full-fledged git-tutorial.

Changing public history

The assumption so far was that all changes were never pushed to a remote repository (like GitHub, GitLab, someone’s else PC, ..) but only committed in your local repository.

It is possible to change remote history, e.g. if you have pushed some sensitive data, but in most cases, you don’t want to. This can cause more problems than leaving your typo in a commit message or creating a new commit for fixing a previous error.