This post tries to provide a gentle introduction to git. While it is aimed at newcomers it is also meant to give a rather good overview and understanding about what one can do with git and thus is extensive. It follows the premise:
If you know how a thing works, you can make it do what you want it to.
Please don’t be afraid as I’m trying to use light language and many examples so you can read fast through it and understand anyway (if I succeed). I’m also trying to highlight important things at least for long texts to ease reading a bit more. However, at some points this tutorial may require you to think for yourselves a bit – it’s a feature, not a bug.
This tutorial is meant for everyone willing to learn git – it does not matter if you are a developer who never really got the hang of it or a student wanting to learn. Even if you are doing non coding tasks like design or documentation git can be a gift from heaven. This tutorial is for everyone – don’t be scared by its length and believe me, it pays off! The only requirement is that you have git installed, know
mkdir and have something to commit – and who doesn’t have any digital data!?
This tutorial is the first out of three tutorials which are meant to free your mind from atraditional view on filesystems.
This tutorial, Genesis (i.e. “Creation” or “Beginning”), will cover some very basic things:
- Configuring git so you can use it how you want. (Basic, aliases)
- Creating your git repository (play god once!)
- Creating your first git commits.
- Learning what the repository is and where commits get stored.
- Browsing through history.
- Ignoring files.
Exodus (i.e. “going out”) will cover:
- Store temporary changes without committing.
- Navigating commits in git.
- Sharing commits to a remote place.
- Locally (huh, that’s sharing?)
- Via email
- To a server
- To a client
- Working with others (i.e. non-linear).
- Join things.
- Linearize things.
- Writing good commits.
- Editing commits. (Actually not editing but it feels like that.)
Apocalypse (i.e. “uncovering”) will try to uncover some more advanced features of git, finally freeing your mind from your non-versioned filesystem:
- Finding more information about code.
- Finding causes of bugs in git.
- Reverting commits.
- Reviewing commits.
- Travelling though time and changing history (you want me to believe you’ve never wanted to do that?)
- Getting back lost things.
- Let git do things automatically.
A short warning: If you ever really got the hang of git you will not be able to use something else without symptoms of frustration and disappointment – you’ll end up writing every document versioned as an excuse to use git.
A warning for windows users: you may need to use equivalent commands to some basic UNIX utilities or just install them with git. (Installer provides an option for that.) In general it’s a bit like travelling with Vogons – avoid when possible.
A warning for GUI users: Don’t use your GUI. Be it the GitHub App or SourceTree or something else – they usually try to make things more abstract for us, thus they hinder us from understanding git and we can then not make git do what we want. Being able to communicate directly with git is a great thing and really bumps productivity!
I wrote this tutorial to the best of my knowledge and experience, if you spot an error or find something important is missing, be sure to drop me a message!
So go now, grab a cup of coffee (or any other drink), a towel, take your best keyboard and open a terminal beneath this window!
Before we really get started it is important to know what git roughly does: git is a program that allows you to manage files. To be more specific git allows you to define changes on files. In the end your repository is just a bunch of changes that may be related to each other.
Before we can continue we’ll have to set up a few tiny things for git. For this we will use the
git config --global command which simply stores a key value pair into your user-global git configuration file (usually stored at
Let’s tell git who we are! This is pretty straightforward:
$ git config --global user.name "Ford Prefect" $ git config --global user.email firstname.lastname@example.org
This makes git store values for “name” and “email” within the “user” section of the gitconfig.
For some operations git will give you an editor so you can enter needed data. This editor is vim by default. Some people think vim is great (vim is great!), some do not. If you belong to the latter group or don’t know what vim is and how to operate it, let’s change the editor:
$ # Take an editor of your choice instead of nano $ git config --global core.editor nano
Please make sure that the command you give to git always starts as an own process and ends only when you finished editing the file. (Some editors might detect running processes, pass the filename to them and exit immediately. Use
-s argument for gedit,
--waitargument for sublime.) Please don’t use notepad on windows, this program is a perfect example of a text editor which is too dumb to show text unless the text is written by itself.
So, lets get started – with nothing. Let’s make an empty directory. You can do that from your usual terminal:
$ mkdir git-tutorial $ cd git-tutorial $ ls -a ./ ../
So, lets do the first git command here:
$ git init Initialized empty Git repository in /home/lasse/prog/git-tutorial/.git/ $ ls -a ./ ../ .git/
So now we’ve got the
.git folder. Since we just created a repository with
git init, so we can deduce, that this .git directory must in fact be the repository!
So, let’s create some content we can manage with git:
$ echo 'Hello World!' >> README $ cat README Hello World!
Since we know, that the .git directory is our repository, we also know that we did not add this file to our repository yet. So how do we do that?
As I’ve hinted before, our git repository does not contain files but only changes – so how do we make a change out of our file?
The answer lies in (1)
git add and (2)
git commit which allow us to (1) specify what files/file changes we want to add to the change and (2) that we want to pack those file changes into a so called commit. Git also offers a helper command so we can see what will be added to our commit:
Let’s try it out:
$ git status On branch master Initial commit Untracked files: (use "git add <file>..." to include in what will be committed) README nothing added to commit but untracked files present (use "git add" to track) $ git add README $ git status On branch master Initial commit Changes to be committed: (use "git rm --cached <file>..." to unstage) new file: README
So obviously with
git add we can stage files. What does that mean?
As we know, when we’re working in our directory any actions on files won’t affect our repository. So in order to add a file to the repository, we’ll have to put it into a commit. In order to do that, we need to specify, what files/changes should go into our commit, i.e. stage them. When we did
git add README, we staged the file README, thus every change we did until now to it will be included in our next commit. (You can also partially stage files so if you edit README now the change won’t be committed.)
Now we’ll do something very special in git – creating the first commit! (We’ll pass the
-vargument to get a bit more info from git on what we’re doing.)
$ git commit -v
You should now get your editor with contents similar to this:
# Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # On branch master # # Initial commit # # Changes to be committed: # new file: README #ref: refs/heads/master # ------------------------ >8 ------------------------ # Do not touch the line above. # Everything below will be removed. diff --git a/README b/README new file mode 100644 index 0000000..c57eff5 --- /dev/null +++ b/README @@ -0,0 +1 @@ +Hello World!
Since we’re about to create a change, git asks us for a description. (Note: Git actually allows to create commits without a description with a special argument. This is not recommended for productive collaborative work!)
Since we passed the
-v parameter, git also shows us below what will be included in our change. We’ll look at this later.
Commit messages are usually written in imperative present tense and should follow certain guidelines. We’ll come to this later.
So, let’s enter:
Add README as our commit message, save and exit the editor.
Now, let’s take a look at what we’ve created,
git show is the command that shows us the most recent commit:
$ git show commit ec6c903a0a18960cd73df18897e56738c4c6bb51 Author: Lasse Schuirmann <email@example.com> Date: Fri Feb 27 14:12:01 2015 +0100 Add README diff --git a/README b/README new file mode 100644 index 0000000..980a0d5 --- /dev/null +++ b/README @@ -0,0 +1 @@ +Hello World!
So what do we see here:
- It seems that commits have an ID, in this case
- Commits also have an author and a creation date.
- Of course they hold the message we wrote and changes to some files.
What we see below the
diff ... line is obviously the change. Let’s take a look at it: since git can only describe changes, it takes
/dev/null (which is a bit special, kind of an empty file, not important here), renames it to
README and fills it with our contents.
So, this commit is pretty godish: It exists purely on it’s own, has no relations to any other commit (yet, it’s based on an empty repository, right?) and creates a file out of nothing (/dev/null is somehow all and nothing, kind of a unix black hole).
So, let’s look in our repository!
$ ls -la .git total 52 drwxrwxr-x. 8 lasse lasse 4096 Feb 27 16:05 ./ drwxrwxr-x. 3 lasse lasse 4096 Feb 27 14:11 ../ drwxrwxr-x. 2 lasse lasse 4096 Feb 27 14:11 branches/ -rw-rw-r--. 1 lasse lasse 486 Feb 27 14:12 COMMIT_EDITMSG -rwxrw-r--. 1 lasse lasse 92 Feb 27 14:11 config* -rw-rw-r--. 1 lasse lasse 73 Feb 27 14:11 description -rw-rw-r--. 1 lasse lasse 23 Feb 27 14:11 HEAD drwxrwxr-x. 2 lasse lasse 4096 Feb 27 14:11 hooks/ -rw-rw-r--. 1 lasse lasse 104 Feb 27 14:11 index drwxrwxr-x. 2 lasse lasse 4096 Feb 27 14:11 info/ drwxrwxr-x. 3 lasse lasse 4096 Feb 27 14:12 logs/ drwxrwxr-x. 7 lasse lasse 4096 Feb 27 14:12 objects/ drwxrwxr-x. 4 lasse lasse 4096 Feb 27 14:11 refs/ $
Now let’s look into it further to get to know what it is a bit more. I will try to cover only important parts here, if you’re interested even deeper, you can try DuckDuckGo or take a look at this: http://git-scm.com/docs/gitrepository-layout
The config file is a similar file to the one where our settings in the beginning got stored. (User and editor configuration, remember?) You can use it to store settings per repository.
The objects directory is an important one: It contains our commits.
One could do a full tutorial on those things but that’s not covered here. If you want that, check out: http://git-scm.com/book/en/v2/Git-Internals-Git-Objects
We just saw the ID of the commit we made:
Now let’s see if we find it in the objects directory:
$ ls .git/objects 98/ b4/ ec/ info/ pack/ $ ls .git/objects/ec 6c903a0a18960cd73df18897e56738c4c6bb51
So, when we create a commit, the contents (including metadata) are hashed and git stores it finely into the objects directory.
That isn’t so complicated at all, is it?
git show accepts a commit ID as an argument. So you could e.g. do
git show ec6c903a0a18960cd73df18897e56738c4c6bb51 instead of
git show if this hash is the current commit.
Investigate what the other two objects are, which are stored in the objects directory. (Ignore the info and pack subdirectory.)
git show again and take a look at the line beginning with “index”. I’m sure you can make sense out of it!
The HEAD file is here so git knows what the current commit is, i.e. with which objects it has to compare the files in the file system to e.g. generate a diff. Let’s look into it:
$ cat .git/HEAD ref: refs/heads/master
So it actually only references to something else.
So let’s take a look into refs/heads/master – what ever this is:
$ cat .git/refs/heads/master ec6c903a0a18960cd73df18897e56738c4c6bb51
HEAD file refers to this
master file which refers to our current commit. We’ll see how that makes sense later.
Now, let’s go on and create another commit. Let’s add something to our README. You can do that by yourself, I’m sure!
Let’s see what we’ve done:
$ git diff diff --git a/README b/README index 980a0d5..c9b319e 100644 --- a/README +++ b/README @@ -1 +1,2 @@ Hello World! +Don't panic!
Let’s commit it. However, since we’re a bit lazy we don’t want to add the README manually again; the commit command has an argument that allows you to auto-stage all changes to all files that are in our repository. (So if you added another file which is not in the repository yet it won’t be staged!)
git commit -a -v
Well, you know the game. Can you come up with a good message on your own?
$ git show commit 7b4977cdfb3f304feffa6fc22de1007dd2bebf26 Author: Lasse Schuirmann <firstname.lastname@example.org> Date: Fri Feb 27 16:39:11 2015 +0100 README: Add usage instructions diff --git a/README b/README index 980a0d5..c9b319e 100644 --- a/README +++ b/README @@ -1 +1,2 @@ Hello World! +Don't panic!
So this commit obviously represents the change from a file named README which contents are stored in object
980a0d5 to a file also named README which contents are stored in object
Let’s see a timeline of what we’ve done:
$ git log commit 7b4977cdfb3f304feffa6fc22de1007dd2bebf26 Author: Lasse Schuirmann <email@example.com> Date: Fri Feb 27 16:39:11 2015 +0100 README: Add usage instructions commit ec6c903a0a18960cd73df18897e56738c4c6bb51 Author: Lasse Schuirmann <firstname.lastname@example.org> Date: Fri Feb 27 14:12:01 2015 +0100 Add README
That looks fairly easy. However I cannot withstand to point out that despite commits look so fine, linearly arranged here, they are actually nothing more than commit objects, floating around in the .git/objects/ directory. So
git log just looks where HEAD points to and recursively asks each commit what it’s parent is (if it has one).
Since every good hitchhiker does know how to travel through time and change events, we’ll learn to do that in the next chapter 😉
It is worth to mention that
git add also accepts directories as an argument. I.e.
git add . recursively adds all files from the current directory.
In order to generally ignore certain patterns of files (e.g. it’s bad practice to commit any generated stuff), one can write a
.gitignore file. This file can look as follows:
README~ # Ignore gedit temporary files *.o # Ignore compiled object files
The exact pattern is defined here: http://git-scm.com/docs/gitignore
Files matching this pattern will:
- Not be added with
git addunless forced with
- Not be shown in
git statusas unstaged
It is usually a good idea to commit the
.gitignore to the repository so all developers don’t need to care about those files.
So, we’ve learned quite some stuff. However git command’s aren’t as intuitive as they could be sometimes. They could be shorter too. So let’s define us some aliases of the commands we know. The ones given here are only suggestions, you should choose the aliases in a way that suits best for you!
If you’re using git much, you might want to add
alias g=git to your
.zshrc or whatever. (On windows you’re a bit screwed. But what did you expect? Really?)
Let’s let git give us our editor since we don’t want to edit just one value:
git config --global --edit
You can add aliases through the
[alias] section, here are the aliases I suggest:
[alias] a = add c = commit -v ca = commit -v -a d = diff dif = diff # Thats a typo :) i = init l = log st = status stat = status
So what did we learn?
We did some basic git commands:
git config: accessing git configuration
git init: creating a repository
git status: getting current status of files, staging and so on
git add: staging files for the commit
git diff: showing the difference between the current commit and what we have on our file system
git commit: writing staged changes to a commit
git log: browsing history
We also learned how git organizes commits, how it stores files and how we can make git ignore files explicitly.
I hope this helped you understanding a bit what git does and what it is. The next tutorial will hopefully cover all the basics. (Some were already hinted here.)