There are tons of articles out there trying to help people to make the best use of the tools they have available: “Google Tricks that Give You Better Search Results”, “There Are More Things in Smartphones and Tablets Than Are Dreamt of in Your Philosophy” and other tips can be extremely helpful. Think about having the best bike that you could possibly buy. What a waste of money would it be if you didn’t know how to use its gears? It would be just like any other bike.

The same happens with Git. It is an excellent tool to version control a project, but knowing only how to add and commit files does not exactly make anyone’s life easier. Being able to check previous commits and revert changes are some of the things that make versioning control great.

checkout, revert, reset and stash

While working on my Tic Tac Toe, I just wanted to throw away all changes I had done — none of them staged — and go back to my last commit. Usually, I would use git checkout <filename>. However, I had changed multiple files, and checking out file by file would be a tedious task. That is the moment when knowing all the tricks a tool can perform makes the life easier.

checkout

When working with branches, checkout is used frequently to check out a branch:

  • Creating and checking out a branch:
# at master branch
$ user@user-env ~/git_tests (master)

# create a branch (-b) named 'test-branch-one' and check it out
git checkout -b test-branch-one

# the branch was created and the HEAD is moved to 'test-branch-one'
$ user@user-env ~/git_tests (test-branch-one)
  • Moving from one branch to another:
$ user@user-env ~/git_tests (test-branch-one)
git checkout master
$ user@user-env ~/git_tests (master)

What checkout does is to change HEAD from one place to another. A little deviation to learn about HEAD:

What goes on Git’s HEAD?

HEAD is a pointer to the last commit of a branch that we are working on. So, if I am at master branch and I have three commits, HEAD will be pointing to the third one:

master branch

                     HEAD
------    ------    ------
| C1 |----| C2 |----| C3 |
------    ------    ------

HEAD is stored in a file inside .git/HEAD:

cat .git/HEAD
ref: refs/heads/master

cat .git/refs/heads/master
<third-commit-hash-number>

When checkout is used to move around branches, it changes where HEAD points to. If test-branch-one has two commits, HEAD will be on the second:

test-branch-one

             HEAD
-------    -------
| BC1 |----| BC2 |
-------    -------
git checkout test-branch-one

cat .git/HEAD
ref: refs/heads/test-branch-one

Back to checkout

  • Checking out previous commits

It is possible to checkout old commits with git checkout <commit>, which puts us in a detached HEAD. The good thing is that it is possible to go back to HEAD without harming the repository.

Considering a repository like the following:

git_tests
|_ .git
|_ first.txt
|_ second.txt
|_ third.txt

All the files inside git_tests directory are empty, an initial commit was made to track all of them, and we are in the master branch.

Make some changes to first.txt

first.txt

First commit

Then add to stage and commit:

git add first.txt
git commit -m "first commit"

Then make some more changes to first.txt:

first.txt

First commit

Second commit

Add and commit:

git add first.txt
git commit -m "second commit"

Take a look at the commits hashes:

git log

commit 239e...
Author: <author>
Date:   <date>

    second commit

commit 473d...
Author: <author>
Date:   <date>

    first commit

Checkout the first commit:

git checkout <first-commit-hash>

You are in 'detached HEAD' state. (...)
HEAD is now at 473d... second commit

Take a look at first.txt

first.txt

First commit

Make some changes from there, in the first.txt file:

first.txt

First commit

Changing file while in detached HEAD

Add and commit, then checkout back to master:

git add .
git commit -m "while in detached HEAD"
git checkout master
Warning: you are leaving 1 commit behind, not connected to
any of your branches:

first.txt

First commit

Second commit

Nothing has changed. And if we look at git log, we are going to see only the two commits we had originally:

git log

commit 239e...
Author: <author>
Date:   <date>

    second commit

commit 473d...
Author: <author>
Date:   <date>

    first commit

Where is the commit I did while I was in detached HEAD?

I could find it using git reflog:

git reflog

239e293 HEAD@{0}: checkout: moving from 473d... to master
(...)
26a739f HEAD@{3}: commit: while in detached head
(...)

That commit exists, but it does not belong in any branch and it did not change anything on the project.

  • Discard changes in a file

With all of this back and forth around previous versions of the project, I think it is pretty clear that checkout does not actually change anything in a repository. It only change the place where HEAD is. That is why git checkout <filename> discards non-staged changes: it just goes back to HEAD (last commit) and, since there is nothing staged — and, consequently, committed — the changes in the specific file are lost. There is no going back.

The exception is the command git checkout <commit> <filename>. In this case, only one file will go back to previous state (to the <commit>). If changes are made to that file and then, staged and committed, those changes will reflect on HEAD.

Checkout an specific file in a previous commit

git checkout 473d... first.txt

Make changes to the file:

first.txt

First commit

Change this file when checking this out in a previous commit

Commit changes:

git add .
git commit -m "change one file from previous commit"

Now, when I run git checkout master, I get a message that I am already in master. The git log shows the last commit and first.txt did change.

After learning all those things about checkout, I thought I should be looking into some other option.

revert

One of the greatest things about versioning control is to have access to the story of a repository. All the commits, the creation of the branches, merging, authors, date etc, it is all there. And revert maintains the story even when reverting to a previous commit. What it does, actually, is to create a new commit.

If I create a new file inside git_tests, add and commit it:

touch i-dont-want-this.txt

git add .
git commit -m "add new file to directory"

Then I decide that I don’t care about this last commit and I just want to go back to the previous commit, I can use revert:

git revert <previous-commit-hash>

This will open the text editor so I can type the commit message:

Revert "change one file in previous commit"

After saving the message and quitting the editor, i-dont-want-this.txt is gone. However, it is part of the story of the repository:

commit 8cb...
Author: <author>
Date:   <date>

    Revert "change one file in previous commit"

commit a4c...
Author: <author>
Date:   <date>

    add new file to directory

# ... older commits

revert reverted the state of the repository to a previous commit, but it kept everything, including the changes introduced in the last commit.

reset

While revert keeps the project story, reset does not. reset will permanently undo the changes. The command can be used in different ways:

  • Discarding staged changes

    If I introduce some changes to the first.txt files:

    first.txt

    First commit
    
    Second commit
    
    I'm going to stage this
    

    Add changes to stage:

    git add first.txt
    

    And reset it back (unstage it)

    git reset
    

    If I open first.txt, I can still see the changes introduced earlier, but they are not staged anymore. If I try to commit that change, I get an alert message saying that there is a modified file that is not staged, so the commit cannot proceed:

    git commit -m "try to commit"
    
    (...)
    Changes not staged for commit:
            modified:   first.txt
    
  • Resetting to a previous commit

    Another use for reset is to go back to a previous commit.

    git reset <commit>
    

    This will change the working directory to the given commit. It will not discard any changes made past that point, but the changes are not going to be staged and all the newer commits will be gone. git log will not show the newer commits anymore. The last one will be the one we are in.

    Even if I had the number of a newer commit, I could not go back using git checkout <recent-commit>.

stash

When I found out about stash, I thought that it would be the best solution to my problem (undoing unstaged changes in multiple files), but it turned out to not be exactly what I wanted.

stash is used if we are working on a branch and want to move to another branch without having to commit any changes. This is possible with git stash save. It does not, however, throw away the changes. It just saves the unstaged changes to stash. Them, it would be necessary to run git stash drop to delete the changes from stash.

Again, it didn’t look like the right choice for my problem.

checkout was the solution…

After quite some time researching about how to undo changes in Git, checkout looked the best solution… So, considering that I am at master branch and I make changes in all files, without adding any of those files to stage, I could use git checkout . This would work exactly like git checkout <filename>, but for all files in the repository. It is a dangerous options if we are not sure that we have committed everything we need to before throwing everything away. In my specific case, it was the best option, though.


apprenticeship

git