Recently, I conducted a workshop about how to go back in time with Git alongside Renaud. Here are the main points that we raised during this session.

Case #1: Delete the Last Commit

The initial Git tree used to illustrate this case is:

* 7ec8248 N - (HEAD -> master) Hello, world!
* 26af837 N - Hello, world
* c9b1299 N - Hello

The goal here is to delete the last commit, so that the resulting tree looks like this:

* 26af837 N - (HEAD -> master) Hello, world
* c9b1299 N - Hello

The easiest way to achieve this is to use the git reset command.

$ git reset --hard 26af837

It sets the current branch to the specified commit. The --hard option discards all changes made after the specified commit.

Case #2: Create a Branch from a Previous Commit

In this case, the initial Git tree is the same as before.

* 7ec8248 N - (HEAD -> master) Hello, world!
* 26af837 N - Hello, world
* c9b1299 N - Hello

We want to create a branch from the 26af837 commit to fix a bug, for instance. The resulting tree should be the following:

* ae77cf0 N - (HEAD -> bug-fix) Fixed it!
| * 7ec8248 N - (master) Hello, world!
|/  
* 26af837 N - Hello, world
* c9b1299 N - Hello

First, we need to position ourselves on the commit:

$ git checkout 26af837

Then, we are in a ‘detached HEAD’ state, which means we are no longer on a branch, and further commits will not be kept. From there, we can create the branch and commit the bug fix:

$ git checkout -b bug-fix
$ # Fix the bug ...
$ git commit -m "Fixed it!"

Case #3: Put the Last Commit on a New Branch

Here, we made a commit on the master branch by mistake and want to transfer it to another branch.

The Git tree should go from this:

* 7ec8248 N - (HEAD -> master) Hello, world!
* 26af837 N - Hello, world
* c9b1299 N - Hello

to this:

* 7ec8248 N - (HEAD -> feature) Hello, world!
* 26af837 N - (master) Hello, world
* c9b1299 N - Hello

To do so, we create the feature branch from the last commit and reset master to the previous one.

$ git checkout -b feature
$ git checkout master
$ git reset --hard 26af837
$ git checkout feature

Case #4: Rewrite History

In this case, we want to remove a past commit from the Git tree completely.

The initial tree looks like this:

* b1b5f0a N - (HEAD -> master) Hello, world
* b7d38ac N - Add key.txt
* c9b1299 N - Hello

We want the resulting tree to show no sign of the commit b7d38ac:

* efb44c9 N - (HEAD -> master) Hello, world
* c9b1299 N - Hello

The git rebase -i command allows you to rewrite the history of a branch from a specific starting point. And, it will prompt the list of all commits since this starting point, letting you perform different actions on these commits, for example, rewording a commit message, squashing several commits into one, and reordering and deleting commits. After these modifications, lines will be executed from top to bottom. You can find more information about rewriting history here.

$ git rebase -i HEAD~2
$ # Delete the line corresponding to the commit to remove.

Since we wanted to go two commits back, we used the notation HEAD~2 to specify the starting point. Alternatively, we could also have used the specific hash of the commit (c9b1299).

Case #5: Revert a Commit

During this workshop, an attendee mentioned the git revert command. Unlike the other commands in this article, git revert does not modify past commits. Instead, it creates a new commit that is the exact opposite of the reverted commit.

For instance, if we start from this Git tree:

* f999291 N - (HEAD -> master) Hello, world
* a9f8fb3 N - Add key.txt
* c9b1299 N - Hello

and revert the commit a9f8fb3, which contains only a new file, a new commit removing this file will be created.

$ git revert a9f8fb3
* 55bc161 N - (HEAD -> master) Revert "Add key.txt"
* f999291 N - Hello, world
* a9f8fb3 N - Add key.txt
* c9b1299 N - Hello

This command could be helpful when several people work on the same branch to avoid a forced push on the remote repository. It also keeps an explicit trace of the action in the tree for a particular reason.

Last resort: Keep Calm and Use Git Reflog

The git reflog command recovers commits that appear to be lost. To illustrate what it does, here is its output when used in case #4:

$ git reflog
efb44c9 HEAD@{0}: rebase -i (finish): returning to refs/heads/master
efb44c9 HEAD@{1}: rebase -i (pick): Hello, world
c9b1299 HEAD@{2}: rebase -i (start): checkout HEAD~2
26af837 HEAD@{3}: commit: Hello, world
712d068 HEAD@{4}: commit: Add key.txt
c9b1299 HEAD@{5}: commit (initial): Hello

You can see that the deleted commit (712d068) still appears in the reflog.

Conclusion

This article shows several ways to go back in time with Git. These commands could be used alone or combined to get you out of complicated situations or to rearrange your Git tree before a git push.