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 commit
ted - 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 -m
essage 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.