A quick Git guide for newbies

I decided to write this post because I believe there are lots of people out there that probably struggle using Git or maybe don’t really know what it is. Speaking for myself, I didn’t use it for several years, mostly because I wasn’t really aware of how useful and powerful it can be! Keep in mind that I am just a Git newbie and this article is mostly a compilation on how to set up a local repository, use it and working a remote repository.

What is Git? Why should I use it?

Git is a distributed version control system (VCS) and a source control management (SCM) tool created by Linus Torvalds in 2005. As a distributed version and source control system, Git allows multiple users to work on a project (a repository) and concurrently contribute to it. Git also works on local repositories instead of relying on a remote server (of course you can work with a remote repository too).

Regarding why to use it, well, did you ever found yourself working on a project, trying to fix a bug or testing a new feature which ended up in a catastrophic error or problem which then took you hours, days or weeks to solve or undo? Well, Git can help you!

Git tracks alterations to any file within a project and allows you to compare files amongst different revisions (or commits in Git jargon) and even returning to project to a previous known-to-work state. It can also start new sandboxes (the branches in Git jargon) where you can test new functionalities or features and then merge them to the master project or simply discard them!

Another important reason to learn and to use Git is that most companies use it and expect current and future employees to use it too! So what you are waiting for?

Git States or Stages

When working with Git, it is important to understand the different states or stages a file can be. First of all, files can be tracked by Git or they can be untracked. Obviously only tracked files will be part of the VCS/SCM. Also, keep in mind that while you can track almost any kind of file, only text files can be really understood by Git, so that it can track the differences between them.

In Git jargon, all tracked files are your working files and they can be located in the same project directory or in any folder within it.

Git operations
Figure 1 – Git States and Operations (Source: wikipedia)

Working files are staged when they are added for the first time or added after a change. The staging area is a cache or buffer which stores all files until you move them to the next state (the commit stage) or get them back to the previous state.

Commit state is like a local hard copy of your files, it can work as a backup but it is much more than that because Git allows you to compare changes between committed and working files. Each commit operation also includes a description (message) of what has changed since the last commit (or, what is the reason for this commit).

Note that Git does not simply store a new version of your files, it really store the differences between current and previous version, so you can track what changed and why it changed. All these information is stored within the .git directory, which is created when you initialize a new Git project.

A local repository can also be used to update or get updates from a remote one.

Getting Started with Git

The first thing to do in order to use Git is installing it (if your computer does not have it already). I am not covering how to do that, because you can easily find information on the Internet. Just keep in mind that I am using Linux command line here, so if you are using Windows, you will probably have to use a Git command line within a cmd box instead.

Also, before creating a project with Git, it is important to set some very important internal variables: your user name and email. These are important to distinguish who changed what within a distributed version control system.

So, let’s first set your user name and email using the following commands:

git config --global user.name "your name here"
git config --global user-email "your email here"

You can check your Git configuration with the following command:

git config --list

Keep in mind that Git can have global configurations, system configurations (per user) and local configurations (per project).

Now that you made the basic Git setup, let’s make Git to be aware of a project we want it to track. All you have to do is to go to your project base directory and from there issue git init command:

fabiop@XPS15 ~/Development $ mkdir gittest
fabiop@XPS15 ~/Development $ cd gittest
fabiop@XPS15 ~/Development $ nano test.c

Note that I just created a “gittest” directory within my “development” directory, then changed to that directory and started nano to type a very simple C program:

#include <stdio.h>

void main (void) {
        printf("Hello world!\r\n");
}

Once you save that program, you can use gcc to compile it, and test your newly test.o binary:

fabiop@XPS15 ~/Development $ gcc test.c -o test
fabiop@XPS15 ~/Development $ ./test
Hello world! 

Now let’s instruct Git to track our small project (git init command):

fabiop@XPS15 ~/Development/gittest $ git init 
Initialized empty Git repository in /home/fabiop/Development/gittest/.git/

Git replied that it successfully created a new empty repository there (a .git directory was just created within my project directory). You can do the same in any other existing project directory.

Now Git is set and will track anything that happens in our project. But as of now, no file is being tracked! We can check that by issuing a git status command:

fabiop@XPS15 ~/Development/gittest $ git status
On branch master

Initial commit

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	test.c
	test

nothing added to commit but untracked files present (use "git add" to track)

Note that Git is reporting we are in our master branch and that there are two untracked files. It also states that we can track a file by issuing a git add command to track a file. In fact, you can track files by specifying each file name or simply instruct Git to track all files. But be careful! Our test.o file is not a text file and in fact it should not be tracked ever! As of now, we could simply issue a git add test.c command to track our C file, but what if we had a lot of files? Well, there is a way to instruct Git to ignore some files and it is by creating a .gitignore file! Let’s create a .gitignore file with just a single line:

test
fabiop@XPS15 ~/Development/gittest $ nano .gitignore
fabiop@XPS15 ~/Development/gittest $ cat .gitignore
test

Note that usually you are going to add all sort of object (*.o) and intermediate files (*.a, *.map, etc) to your gitignore!

Now we can issue a git add . command to add all files (except for those explicitly listed in the .gitignore file):

fabiop@XPS15 ~/Development/gittest $ git add .
fabiop@XPS15 ~/Development/gittest $ git status
On branch master

Initial commit

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

	new file:   .gitignore
	new file:   test.c

Note that now we have two files staged: .gitignore and test.c, test.o was left out and will not be tracked!

From here, we can work and change our test.c file (which would lead to a new changed file, with the same file appearing in the staging section as well) or we can commit our changes and create our first “hard copy” of the project. Note that as our project was not committed yet, we can unstage any file by issuing a git rm –cached command on the desired file.

Commit

Now that we have some staged files, it is time to commit them, this will save the current state of our project. We can do that by issuing a git commit command (which will open a text editor for inputting the commit message) or you can use git commit -m “your commit message” instead. The -m instructs Git to use the supplied message instead of ask the user for a commit message:

fabiop@XPS15 ~/Development/gittest $ git commit -m "First commit"
[master (root-commit) 7dc24ac] First commit
 2 files changed, 7 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 test.c

Congratulations on your first commit! We can issue a git status command to make sure everything is fine:

fabiop@XPS15 ~/Development/gittest $ git status
On branch master
nothing to commit, working directory clean

From now on, every time you modify your project you can commit the changes so they can be registered and stored by Git.

But what if I want to undo or change a working file to a previous state? This is when you can use git checkout command! Suppose you changed your test.c file and now you want to restore it to the way it was in our first commit. All you have to do is to issue a git checkout command:

fabiop@XPS15 ~/Development/gittest $ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   test.c

no changes added to commit (use "git add" and/or "git commit -a")
fabiop@XPS15 ~/Development/gittest $ git checkout test.c

Now if you check your test.c file, its contents will be exactly what it was in the last commit! Cool isn’t it?

You can also unstage files by using the git reset HEAD command. Suppose you have staged a file named test2.c and now you don’t want to include it anymore:

fabiop@XPS15 ~/Development/gittest $ git reset HEAD test2.c
fabiop@XPS15 ~/Development/gittest $ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)

	test2.c

nothing added to commit but untracked files present (use "git add" to track)

Git can also track file deletions. You can delete a file from Git by using git rm command (which deletes the file and stages that information) or you can delete a file using your file system and then staging that change on Git:

fabiop@XPS15 ~/Development/gittest $ git rm test2.c
rm 'test2.c'
fabiop@XPS15 ~/Development/gittest $ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	deleted:    test2.c

fabiop@XPS15 ~/Development/gittest $ ls
test.c  test.o

Or you could delete it on your file system and then stage that deletion:

fabiop@XPS15 ~/Development/gittest $ rm test2.c
fabiop@XPS15 ~/Development/gittest $ ls
test.c  test.o
fabiop@XPS15 ~/Development/gittest $ git status
On branch master
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	deleted:    test2.c

no changes added to commit (use "git add" and/or "git commit -a")
fabiop@XPS15 ~/Development/gittest $ git add test2.c
fabiop@XPS15 ~/Development/gittest $ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	deleted:    test2.c

Anyway, keep in mind that only after you commit those changes, they will be stored into your local repository!

Now as we continue working in our project, we are going to make more changes to it and commit them, suppose we added some code to our test.c.

#include <stdio.h>

void main (void) {
        printf("Hello world!\r\n");
        printf("New test\r\n");
}

In order to store the new changes we must add the file to the staging area and commit it:

fabiop@XPS15 ~/Development/gittest $ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   test.c

no changes added to commit (use "git add" and/or "git commit -a")
fabiop@XPS15 ~/Development/gittest $ git add .
fabiop@XPS15 ~/Development/gittest $ git commit -m "new printf added"
[master 65b7522] new printf added
 1 file changed, 1 insertion(+)
fabiop@XPS15 ~/Development/gittest $ git log
commit 65b75228d0638db2af34326f8b81b15f764b7a5b
Author: Fábio Pereira <nobody@nowhere.com>
Date:   Sun Jun 18 14:17:54 2017 -0400

    new printf added

commit 7dc24ac2c3ddf55ea3ecf62a7c51d8c386623065
Author: Fábio Pereira <nobody@nowhere.com>
Date:   Sun Jun 18 14:15:36 2017 -0400

    First commit

Can you see I used git log in order to get a list of all commits we have? Each commit includes a hash hexadecimal number, who made the change, the date and time and also the commit message. That hash code can be used with a Git checkout command in order to switch to a previous commit as we will see later.

Branches

One of the very nice things about Git is the ability to create and work with branches. A Git branch is a snapshot of your project from which you can work without disturbing your main project. This is very useful when creating or testing new features on an existing project. You can get a list of available branches at any time by using the git branch command:

fabiop@XPS15 ~/Development/gittest $ git branch
* master

As of now, our project has only one branch, which is known as master branch. The star in front of it shows that we are currently in that branch. Let’s create a new branch (version2) to include a new feature:

fabiop@XPS15 ~/Development/gittest $ git branch version2
fabiop@XPS15 ~/Development/gittest $ git branch
* master
  version2

Now we have to move our environment to that version2 branch by issuing a git checkout command:

fabiop@XPS15 ~/Development/gittest $ git checkout version2
Switched to branch 'version2'
fabiop@XPS15 ~/Development/gittest $ git branch
  master
* version2

From now on, all changes we do will affect only version2 branch, so let’s change our test.c code:

#include <stdio.h>

void main (void) {
        printf("Hello world!\r\n");
        printf("New test\r\n");
        for (int temp=0; temp<10; temp++) printf("%i\r\n",temp);
}

After changing our test.c file, we can type these commands:

fabiop@XPS15 ~/Development/gittest $ git status
On branch version2
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   test.c

no changes added to commit (use "git add" and/or "git commit -a")
fabiop@XPS15 ~/Development/gittest $ git add .
fabiop@XPS15 ~/Development/gittest $ git commit -m "added counter from 0 to 9"
[version2 7476c9b] added counter from 0 to 9
 1 file changed, 1 insertion(+), 1 deletion(-)

Now if we look at our commit log, we will see this:

fabiop@XPS15 ~/Development/gittest $ git log
commit 22c50f54195cfe1a9838d49d5fba4b0b80ae1fce
Author: Fábio Pereira <fabio.jve@gmail.com>
Date:   Sun Jun 18 14:30:36 2017 -0400

    added counter from 0 to 9

commit 65b75228d0638db2af34326f8b81b15f764b7a5b
Author: Fábio Pereira <fabio.jve@gmail.com>
Date:   Sun Jun 18 14:17:54 2017 -0400

    new printf added

commit 7dc24ac2c3ddf55ea3ecf62a7c51d8c386623065
Author: Fábio Pereira <fabio.jve@gmail.com>
Date:   Sun Jun 18 14:15:36 2017 -0400

    First commit

We can move back and forth between our version2 and master branch without loosing any piece of code:

fabiop@XPS15 ~/Development/gittest $ gcc test.c -o test.o
fabiop@XPS15 ~/Development/gittest $ ./test.o
Hello world!
0
1
2
3
4
5
6
7
8
9
fabiop@XPS15 ~/Development/gittest $ git checkout master
Switched to branch 'master'
fabiop@XPS15 ~/Development/gittest $ gcc test.c -o test.o
fabiop@XPS15 ~/Development/gittest $ ./test.o
Hello world!
New test

Nice isn’t it?

Another usage of checkout is switching to a previous commit within a branch. Let’s suppose we wanted to switch to the first commit within version2 branch. First we can use git branch to check our current branch and then change to the desired branch with git checkout:

fabiop@XPS15 ~/Development/gittest $ git branch
* master
  version2
fabiop@XPS15 ~/Development/gittest $ git checkout version2
Switched to branch 'version2'

Now we can use git log command to list all commits within the current branch and then use Git checkout to switch to one specific commit:

fabiop@XPS15 ~/Development/gittest $ git log
commit 7476c9b7cf07b24356a6dfcb5f299c7098348470
Author: Fábio Pereira <fabio.jve@gmail.com>
Date:   Sun Jun 18 14:30:15 2017 -0400

    added counter from 0 to 9

commit 65b75228d0638db2af34326f8b81b15f764b7a5b
Author: Fábio Pereira <fabio.jve@gmail.com>
Date:   Sun Jun 18 14:17:54 2017 -0400

    new printf added

commit 7dc24ac2c3ddf55ea3ecf62a7c51d8c386623065
Author: Fábio Pereira <fabio.jve@gmail.com>
Date:   Sun Jun 18 14:15:36 2017 -0400

    First commit
fabiop@XPS15 ~/Development/gittest $ git checkout 7dc24ac2c3ddf55ea3ecf62a7c51d8c386623065
Note: checking out '7dc24ac2c3ddf55ea3ecf62a7c51d8c386623065'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b <new-branch-name>

HEAD is now at 7dc24ac... First commit

Note that Git warns us that we are now in a “detached HEAD” state, meaning we have just created a new temporary branch. We can issue a git checkout -b command to create a new branch or just use this temporary branch as a sandbox to test any ideas!

If we issue a git log and a git status command we will get the following result:

fabiop@XPS15 ~/Development/gittest $ git log
commit 7dc24ac2c3ddf55ea3ecf62a7c51d8c386623065
Author: Fábio Pereira <fabio.jve@gmail.com>
Date:   Sun Jun 18 14:15:36 2017 -0400

    First commit
fabiop@XPS15 ~/Development/gittest $ git branch
* (HEAD detached at 7dc24ac)
  master
  version2

But if we choose to create a new branch (instead of that temporary one), we could have something like:

fabiop@XPS15 ~/Development/gittest $ git checkout -b version3
Switched to a new branch 'version3'
fabiop@XPS15 ~/Development/gittest $ git branch
  master
  version2
* version3

Now we have three branches and instead of work in that detached branch we can now work in version3 branch.

But what if I don’t want to use that version3 branch anymore and I want to delete it? You can use git branch -d command:

fabiop@XPS15 ~/Development/gittest $ git branch -d version3
error: Cannot delete the branch 'version3' which you are currently on.
fabiop@XPS15 ~/Development/gittest $ git checkout master
Switched to branch 'master'
fabiop@XPS15 ~/Development/gittest $ git branch -d version3
Deleted branch version3 (was 7dc24ac).
fabiop@XPS15 ~/Development/gittest $ git branch
* master
  version2

Note that you can’t delete a branch while you are currently using it!

For now, let’s just return to our master branch:

fabiop@XPS15 ~/Development/gittest $ git checkout master
Previous HEAD position was 7dc24ac... First commit
Switched to branch 'master'

 

Remote Repositories

Now that we know how to work with our local repository (or simply repo), it is time to learn how to deal with remote ones. There are several Git servers you can use and probably the first and most famous is GitHub. GitHub provides unlimited storage space for public repositories but charges for private ones. An interesting alternative to GitHub is BitBucket: it provides free storage for small teams (up to five users and up to 1GB per month) and allows both private and public repositories! I am going to use Bitbucket in this section but you can use whatever Git repository server you want.

The first thing to know is that in order to use a remote repository it must exist (or you can create it) and you must have access to change it. If you only need to copy it and use it as a starting point for your own project or repository, you can clone a remote repository. Let’s create a new repository in Bitbucket:

After creating the repo, you will get the following screen:

Now all you have to do is to copy your remote repository address (shown in the top right-corner of your repository overview) and instruct Git to use it as your remote repository with the command git remote: git remote add origin https://YOURUSER@bitbucket.org/YOURUSER/REPOSITORYNAME.git . And then issue a push command git push u origin master command to push data into the remote server. You are going to be asked for your bitbucket password and after a while, Git will reply with something like this:

fabiop@XPS15 ~/Development/gittest $ git remote add origin https://fabiopjve@bitbucket.org/fabiopjve/gittest.git
fabiop@XPS15 ~/Development/gittest $ git push -u origin master
Password for 'https://fabiopjve@bitbucket.org': 
Counting objects: 10, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (8/8), done.
Writing objects: 100% (10/10), 825 bytes | 0 bytes/s, done.
Total 10 (delta 2), reused 0 (delta 0)
To https://fabiopjve@bitbucket.org/fabiopjve/gittest.git
 * [new branch]      master -> master
Branch master set up to track remote branch master from https://fabiopjve@bitbucket.org/fabiopjve/gittest.git.

From now on, every time you need to push new commits to your Bitbucket repository, you just have to issue a git push command:

fabiop@XPS15 ~/Development/gittest $ git push

And to pull from your remote repository you can use a git pull command:

fabiop@XPS15 ~/Development/gittest $ git pull
Password for 'https://fabiopjve@bitbucket.org': 
From https://bitbucket.org/fabiopjve/gittest
 * branch            master     -> FETCH_HEAD
Already up-to-date.

Once we have up-to-date local and remote repositories, let’s add a README.md file, but this time, we will create it using Bitbucket’s web interface. Go to your repository overview and press the “create a README” button:

Just use the template from Bitbucket and save it. Now your remote repository should have a README.md file which is not present in your local repo!

In order to synchronize your local repo with your remote one, all you have to do is to issue a git pull command:

fabiop@XPS15 ~/Development/gittest $ git pull
Password for 'https://fabiopjve@bitbucket.org': 
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From https://bitbucket.org/fabiopjve/gittest
 * branch            master     -> FETCH_HEAD
Updating 0a17f0a..29c08da
Fast-forward
 README.md | 29 +++++++++++++++++++++++++++++
 1 file changed, 29 insertions(+)
 create mode 100644 README.md

Our newly created README.md should be present within our project files:

fabiop@XPS15 ~/Development/gittest $ ls
README.md  test.c  test.o

But if we pay attention to our remote repo, we will notice something wrong: we only have our master branch, where is the version2 branch? The answer is simple: it is still in our local repo, but was not pushed along with master to our remote repo. That’s because our git push command only pushed our current (master) repository. In order to push all branches we must include the –all option: git push –all

That brings another interesting question: what if I wanted to delete a branch?

We already saw that we can use git branch -d to delete a local repository, but it won’t affect the branch on a remote repository. In order to do that, you must use git push origin :BRANCHNAME command. So in order to delete version2 branch in the remote repo we could issue:

fabiop@XPS15 ~/Development/gittest $ git push origin :version2
Password for 'https://fabiopjve@bitbucket.org': 
To https://fabiopjve@bitbucket.org/fabiopjve/gittest.git
 - [deleted]         version2

That is it! I believe this is a good starting point for anyone wanting to learn and use Git.

References

Git Official Book

Git Tutorials and Training at Atlassian.com

Learning Git and GitHub at Lynda.com

Git Essential Training at Lynda.com

Leave a Reply