Getting Started With Git


In this video we are going to take a beginners guide to Git, using a brand new Symfony project as our starting point. I am only using Symfony as my example as this site is predominantly related to Symfony, but of course, Git can be used for any code project in the same way.

The commands I cover in this video are those I use most frequently when working with Git.

By the end of this video you will have an understanding of how to:

  • Initialise a new Git repository (aka start using Git in your project);
  • Stop certain files and folders from being tracked by Git (.gitignore);
  • Add and remove items from your next commit;
  • Push your code to a remote site such as Github, Gitlab, Bitbucket, or similar

There's plenty to cover in this video, so let's not delay any further.

Gitting Going

I am going to assume you have installed Git. After installing is really where the fun starts.

There's definitely a learning curve to getting started with Git, and it really does help to have someone more experienced available to help you when you get stuck.

That said, Git is used on so many projects these days that pretty much any situation you find yourself in will likely have an extensive StackOverflow answer (or likely, answers) to get your issue resolved.

Truthfully, most of the problems I have ever encountered with Git have been as a result of merge conflicts. A merge conflict is where two or more changes have been made to a particular line (or lines) of code in a file, and you must determine which change is ultimately correct.

This is not something we need to worry about initially. In this video we are thinking about Git in a Single Player environment. We aren't covering branches, or working in a team yet. That will come in the very next video. I think of that as Git in Multiplayer mode :)

As mentioned earlier, we will be using the symfony new git-example command to get going.

➜  Development symfony new git-example

 Downloading Symfony...

    5.3 MiB/5.3 MiB ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  100%

 Preparing project...

 ✔  Symfony 3.2.2 was successfully installed. Now you can:

 ... (rest removed)

Once we've run that command, we can change into the newly created directory:

➜  Development cd git-example
➜  git-example ls -la
total 128
drwxr-xr-x  9 chris chris  4096 Feb  6 09:53 .
drwxrwxr-x 50 chris chris  4096 Feb  6 09:53 ..
drwxr-xr-x  4 chris chris  4096 Feb  6 09:53 app
drwxr-xr-x  2 chris chris  4096 Feb  6 09:53 bin
-rw-rw-rw-  1 chris chris  2075 Feb  6 09:53 composer.json
-rw-rw-rw-  1 chris chris 75375 Feb  6 09:53 composer.lock
-rw-r--r--  1 chris chris   248 Jan 12 21:48 .gitignore
-rw-r--r--  1 chris chris   978 Jan 12 21:48 phpunit.xml.dist
-rw-rw-rw-  1 chris chris    81 Feb  6 09:53 README.md
drwxr-xr-x  3 chris chris  4096 Feb  6 09:53 src
drwxr-xr-x  3 chris chris  4096 Feb  6 09:53 tests
drwxr-xr-x  5 chris chris  4096 Feb  6 09:53 var
drwxr-xr-x 15 chris chris  4096 Feb  6 09:53 vendor
drwxr-xr-x  3 chris chris  4096 Jan 12 21:50 web

At this point our project is not yet being tracked by Git.

But as Git is so widely used, Symfony do provide us with a default .gitignore file.

As the name implies, this file tells Git which files and directories that Git should ignore. Looking at the contents of this file is interesting for a couple of reasons:

➜  git-example cat .gitignore
/app/config/parameters.yml
/build/
/phpunit.xml
/var/*
!/var/cache
/var/cache/*
!var/cache/.gitkeep
!/var/logs
/var/logs/*
!var/logs/.gitkeep
!/var/sessions
/var/sessions/*
!var/sessions/.gitkeep
!var/SymfonyRequirements.php
/vendor/
/web/bundles/

We don't want to track the cache, logs, and sessions related files and folders. These frequently change, and the contents are not directly important to us as developers. If we did add these files to our Git project, every time we went to commit our code, we might find a whole bunch of changed files that were directly unrelated to our current change. Eliminating noise is a really important part of productively using Git.

Also, the very first line: /app/config/parameters.yml tells us that we do not want to commit a file that potentially contains a bunch of sensitive information - db username, password, and other credentials. You can learn a little more about the solution to this problem in the official docs.

Lastly, notice that the /vendor directory is ignored. This is important as we won't be committing a bunch of third party code to our repository. Not only does this keep the project repository size down (and eliminates more noise), but we should be relying on the composer.lock file to recreate the contents of the /vendor directory anyway.

None of this has any impact until we tell Git about our project.

Initialising Quantum Warp Field

The very first command we need is:

➜  git-example git init
Initialised empty Git repository in /home/chris/Development/git-example/.git/

As noted, this creates a new (hidden) directory in our project - the .git directory.

Interestingly, if you ever wanted to remove Git from your project then all you need to do is delete the .git directory. This can be useful if you ever wish to hard fork someone else's project.

This folder contains everything related to Git in your project. There's a great deal happening in this directory, but thankfully, you can use Git for a very long while without ever needing to go inside this folder at all.

Once you've initialised the directory to be tracked by Git, you need to tell Git about which files to track. when working with developers who are new to Git, I have found that this is somewhat unexpected. We've just told Git to start tracking our directory, and yet it hasn't actually begun doing that?

In order to address that question, let's take a look at the command you will likely run more than any other:

➜  git-example git:(master) ✗ git status
On branch master

Initial commit

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

    .gitignore
    README.md
    app/
    bin/
    composer.json
    composer.lock
    phpunit.xml.dist
    src/
    tests/
    var/
    web/

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

Git expects us to be explicit.

If we want it to track a file, we must explicitly tell it to do so.

As we shall soon see, we even need to tell Git to track specific changes to specific files. This explicitness is part of the power of Git.

Anyway, what's happening here is that Git has taken a note of everything in our working directory, combined this with the files we stated that it should ignore (via .gitignore) and come up with a list of files it can see, but isn't currently keeping track of.

One of the really nice features of Git when used from the command line is that it tries to help us. Here we are being told that we must git add the files we wish to track.

We could go through and add each file one by one, but there is a shortcut. Now, this shortcut should only be used - in my opinion - after doing the git init command. A better command in every other circumstance will be covered shortly.

➜  git-example git:(master) ✗ git add .
➜  git-example git:(master) ✗ git status
On branch master

Initial commit

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

    new file:   .gitignore
    new file:   README.md
    new file:   app/.htaccess
    new file:   app/AppCache.php
    new file:   app/AppKernel.php
    new file:   app/Resources/views/base.html.twig
    new file:   app/Resources/views/default/index.html.twig
    new file:   app/autoload.php
    new file:   app/config/config.yml
    new file:   app/config/config_dev.yml
    new file:   app/config/config_prod.yml
    new file:   app/config/config_test.yml
    new file:   app/config/parameters.yml.dist
    new file:   app/config/routing.yml
    new file:   app/config/routing_dev.yml
    new file:   app/config/security.yml
    new file:   app/config/services.yml
    new file:   bin/console
    new file:   bin/symfony_requirements
    new file:   composer.json
    new file:   composer.lock
    new file:   phpunit.xml.dist
    new file:   src/.htaccess
    new file:   src/AppBundle/AppBundle.php
    new file:   src/AppBundle/Controller/DefaultController.php
    new file:   tests/AppBundle/Controller/DefaultControllerTest.php
    new file:   var/SymfonyRequirements.php
    new file:   var/cache/.gitkeep
    new file:   var/logs/.gitkeep
    new file:   var/sessions/.gitkeep
    new file:   web/.htaccess
    new file:   web/app.php
    new file:   web/app_dev.php
    new file:   web/apple-touch-icon.png
    new file:   web/config.php
    new file:   web/favicon.ico
    new file:   web/robots.txt

Ok, so git add . adds all untracked files to our Staging area.

These files are to be included in our next (our first, in this case) commit.

Again, these files are only currently Staged for commit. We haven't yet committed them.

Another point often raised by developers new to Git / Symfony at this point is: "where are all the files?"

Yeah, good question: Symfony is, like, 7000+ files and folders right? Why is Git only seeing 37 files?

Remember the .gitignore file? Symfony defaults to ignoring everything but the 'stuff' that we change as part of our project. Noise reduction.

At this point our directory is being tracked for changes by Git. We have told Git we want to track these 37 files. But we haven't yet saved - or committed - that information to our Git repository.

Saving Things With git commit

Before we do a git commit, it's important to understand that the outcome of a git commit is a point-in-time recording / snapshot of our project.

By adding (and removing) files, or (c)hunks of code from a commit, we can be extremely precise about what this point-in-time snapshot representation will look like.

Each of these snapshots is represented by a commit hash. You will very likely already have experienced Git's commit hashes, even if you haven't yet realised it.

For example, head on over to Github, for any project, and you will see something akin to:

"Latest commit 2eaab3d 2 days ago"

Here, the commit hash would be 2eaab3d. This string is the unique shorthand representation a much larger string, something like: 2eaab3d2639c60d239c73439d5a18bfe33e282f1.

The shorthand version is designed to be easier for humans. But even so, humans do better with words.

We can use these commit hashes to jump to any previous snapshot along the project's time line. This is a more advanced concept, but knowing it is available is useful all the same.

Anyway, we will create our first snapshot / commit by running git commit now. We will also use the -m flag, allowing us to specify a -message to describe the change added in this commit. Entire articles have been written about what a git commit message should look like, so I will point you in the direction of Google's most popular result and let you read further.

➜  git-example git:(master) ✗ git commit -m "initial commit"
[master (root-commit) 2228ed6] initial commit
 37 files changed, 4183 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 README.md
 create mode 100644 app/.htaccess
 create mode 100644 app/AppCache.php
 create mode 100644 app/AppKernel.php
 create mode 100644 app/Resources/views/base.html.twig
 create mode 100644 app/Resources/views/default/index.html.twig
 create mode 100644 app/autoload.php
 create mode 100644 app/config/config.yml
 create mode 100644 app/config/config_dev.yml
 create mode 100644 app/config/config_prod.yml
 create mode 100644 app/config/config_test.yml
 create mode 100644 app/config/parameters.yml.dist
 create mode 100644 app/config/routing.yml
 create mode 100644 app/config/routing_dev.yml
 create mode 100644 app/config/security.yml
 create mode 100644 app/config/services.yml
 create mode 100755 bin/console
 create mode 100755 bin/symfony_requirements
 create mode 100644 composer.json
 create mode 100644 composer.lock
 create mode 100644 phpunit.xml.dist
 create mode 100644 src/.htaccess
 create mode 100644 src/AppBundle/AppBundle.php
 create mode 100644 src/AppBundle/Controller/DefaultController.php
 create mode 100644 tests/AppBundle/Controller/DefaultControllerTest.php
 create mode 100644 var/SymfonyRequirements.php
 create mode 100644 var/cache/.gitkeep
 create mode 100644 var/logs/.gitkeep
 create mode 100644 var/sessions/.gitkeep
 create mode 100644 web/.htaccess
 create mode 100644 web/app.php
 create mode 100644 web/app_dev.php
 create mode 100644 web/apple-touch-icon.png
 create mode 100644 web/config.php
 create mode 100644 web/favicon.ico
 create mode 100644 web/robots.txt

➜  git-example git:(master) git status
On branch master
nothing to commit, working directory clean

Congratulations, you've just taken your very first steps with Git.

Your IDE (specifically PHPStorm) and Git

Worth noting at this point is that if using an IDE such as PHPStorm, as soon as you create the project, you will end up with extra 'gumf' added to your project's root directory. Specifically with PHPStorm this will be a folder called .idea.

It is a good idea (ho ho) to add this directory to your .gitignore file.

➜  git-example git:(master) ✗ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)

    .idea/

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

After adding the new line to your .gitignore file:

➜  git-example git:(master) ✗ cat .gitignore
/app/config/parameters.yml
/build/
/phpunit.xml
/var/*
!/var/cache
/var/cache/*
!var/cache/.gitkeep
!/var/logs
/var/logs/*
!var/logs/.gitkeep
!/var/sessions
/var/sessions/*
!var/sessions/.gitkeep
!var/SymfonyRequirements.php
/vendor/
/web/bundles/

.idea/

A subsequent git status reveals a new change:

➜  git-example git:(master) ✗ 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:   .gitignore

no changes added to commit (use "git add" and/or "git commit -a")

You may be tempted to do another git add ., but be careful with this.

git add . will add every single change you've made to your staging area.

Potentially this will include changes you do not want - line breaks, comments, snippets of test code and ideas. Your goal should be to eliminate as much noise from your commits as possible. In this way, much later in the future when you inevitably review your commit history for some specific change, you will thank yourself for your good house keeping.

As mentioned earlier, it pays to be explicit with Git.

We want to be as specific as possible with the changes we want staging, and ultimately committing. We can do better.

Better Change Management With git add -p

In our bid to make our commits as clean and as accurate as possible, we want to only include the absolutely most relevant parts of any given change.

In a real world project, the chances are that as you work through a problem in your codebase, you will make more changes than are ultimately required to fix the overall issue. This might be extra comments, additional unused lines of code, or even entire files that turn out to be no longer be required.

If we were to git add . we would capture everything.

By being more selectively we can pick out only the changes that really matter.

Getting into this habit from the start is a good idea:

➜  git-example git:(master) ✗ git add -p

diff --git a/.gitignore b/.gitignore

index 93821ad..1637beb 100644

--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,5 @@
 !var/SymfonyRequirements.php
 /vendor/
 /web/bundles/

+
+.idea

\ No newline at end of file

Stage this hunk [y,n,q,a,d,/,e,?]?

At this point it really does start paying dividends to have some nice terminal colouring / highlighting.

I've added extra line breaks here to increase clarity, as I will be the first to admit this is initially confusing.

By running the command git add -p we can interactively step through each of the changes we've made to our codebase since the previous commit.

In this example we can see Git has noticed a difference between the .gitignore file in our index, and our working directory.

This change is indicated by the + sign as line additions.

Then the command to stage this hunk? By passing in the ? here we can reveal all the available options:

Stage this hunk [y,n,q,a,d,/,e,?]? ?
y - stage this hunk
n - do not stage this hunk
q - quit; do not stage this hunk or any of the remaining ones
a - stage this hunk and all later hunks in the file
d - do not stage this hunk or any of the later hunks in the file
g - select a hunk to go to
/ - search for a hunk matching the given regex
j - leave this hunk undecided, see next undecided hunk
J - leave this hunk undecided, see next hunk
k - leave this hunk undecided, see previous undecided hunk
K - leave this hunk undecided, see previous hunk
s - split the current hunk into smaller hunks
e - manually edit the current hunk
? - print help

The ones I use most frequently being y, n, and s.

y being to add this change to the staging area.

n being to ignore this change.

s being to split the change into smaller (c)hunks, of code, which may help in pulling out just the bits you want. This is more useful in real world projects than it appears here in an example.

What this means is that we can add only parts of a file to the staging area for our next commit:

Stage this hunk [y,n,q,a,d,/,e,?]? y

➜  git-example git:(master) ✗ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    modified:   .gitignore

Backtracking With git reset -p

Adding files, or parts of files in this selective manner is extremely useful.

Sometimes however, you will either make a mistake, or change your mind.

Being able to selectively undo is also possible via git reset -p.

This command works in the opposite fashion - pressing y to remove (c)hunks of code from the staging area, and n to keep the changes in the staging area.

Once you have selectively staged each piece, running a git commit -m "commit message here" will save off our changes to our repository as already covered.

However, you may still have unstaged / untracked files at this point - particularly if you've been trying out ideas. Deleting any untracked files is easy enough - just do that, delete them in your normal manner.

But say you have an existing tracked file that you've made some now-unwanted changes too. How to revert this file to a good state?

Somewhat unintuitively one way to resolve this problem is to checkout the file again from the repository.

Remember, our git repository keeps track of our files as they were at a specific point in time. If we have made a change to a file, but not yet committed it, then this file at the point in time of the last commit would be the file without changes. By checking that file out again from the last commit, it would be reverted to that state it was at then.

For example, to revert any changes to the README.md file, we could check the file out from the previous commit with:

➜  git-example git:(master) ✗ git checkout README.md

The downside to this is that Git is somewhat hardcore. If you do this, then realise you have checked out the wrong file, or wanted some of the changes, or whatever, you are somewhat out of luck.

Unless, that is, you use PHPStorm. In which case the VCS > Local History should save your bacon.

Pushing Code To GitHub / GitLab / BitBucket / Etc

At this point we have the basic working knowledge required to work with Git.

The last core piece to cover is setting up, and pushing our code to a remote repository.

This need not be a cause for alarm. Firstly, let's create a new repository for our code on GitHub. You can do this on your preferred service of choice, as the process is largely identical.

After doing so, Github will instruct us on how to add a remote to our project:

git remote add origin git@github.com:codereviewvideos/git-example.git
git push -u origin master

The one change I would make to this is to change origin to github. You may disagree. I dislike the name origin as in my opinion, it is misleading.

git remote add github git@github.com:codereviewvideos/git-example.git
git push -u origin master

If you follow the instructions then wish to change this, you can run:

git remote rm origin

Then re-run the instructions with a better name as needed.

Once we are happy with the remote name, we can run the commands as needed to push our code to Github:

➜  git-example git:(master) git remote add github git@github.com:codereviewvideos/git-example.git
➜  git-example git:(master) git push -u github master

Counting objects: 51, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (44/44), done.
Writing objects: 100% (51/51), 40.39 KiB | 0 bytes/s, done.
Total 51 (delta 0), reused 0 (delta 0)
To git@github.com:codereviewvideos/git-example.git
 * [new branch]      master -> master
Branch master set up to track remote branch master from origin.

Working With A Team

This covers all the commands you need to get started with Git.

Crucially we have ignored the concept of Branches.

Branches are incredibly useful whether working individually, or as a team. Though I would say they become even more useful / necessary when working in a team.

As mentioned at the start of this write up, Branches are also a potential point of pain.

We will cover branching / team work / multiplayer git in the very next video. But for now, I would recommend you create a new test project and play around with the various commands from this video. You can do most, if not all of this from your IDE, or specific Git GUI client, but I would strongly recommend you stick to the command line. Git is, after all, designed to be used this way.

Better Git Support In Your Terminal

In this, and every other video on this site, I make use of the zsh shell. On top of this, I use ohmyzsh, which enhances the terminal further with autocompletions for many popular Unix commands, and specifically relevant to this video, git. You can see all the shortcuts. I only use a subset, but I highly recommend this - or similar - as it's such as timesaver.

Episodes

# Title Duration
1 Getting Started With Git 07:30
2 Beginners Guide to Git Branching 11:58