If you have any experience with programming or just altering config files, I'm sure you've been dumbstruck by how one change you've made along the line affects the whole project. Identifying and isolating the problem without a version control system is often time- and energy-intensive, involving retracing your steps and checking all changes made before the unwanted behavior first occurred. A version control system is designed explicitly to make that process easier and provide readable comparisons between versions of text.
Another great feature that distributed version control systems such as git provide is the power of lateral movement. Traditionally, a team of programmers would implement features linearly. This meant pulling the code from the trusted source (server) and developing a section before pushing the altered version back upstream to the server. With distributed systems, every computer maintains a full repository, which means each programmer has a full history of additions, deletions and contributors as well as the ability to roll back to a previous version or break away from the trusted repository and fork the development tree (which I discuss later).
The great thing about git is there's so little you need to know! Without further ado, let's begin with the most important commands.
First, I'm working with a previous project of mine located here:
[user@lj src]$ pwd
/home/lj/projects/java/spaceInvaders/src
To create a local repository, simply run:
[user@lj src]$ git init
Initialized empty Git repository in
↪/home/lj/projects/java/spaceInvaders/src/.git/
To add all source files recursively to git's index, run:
[user@lj src]$ git add .
To push these indexed files to the local repository, run:
[user@lj src]$ git commit
You'll see a screen containing information about the commit, which allows you to leave a description of the commit:
# 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: engine/collisionChecker.java
new file: engine/direction.java
new file: engine/gameEngine.java
new file: engine/gameObjects.java
new file: engine/level.java
new file: engine/main.java
new file: engine/mathVector.java
new file: graphics/drawer.java
new file: sprites/baseSprite.java
new file: sprites/boss.java
new file: sprites/enemy.java
new file: sprites/healthBar.java
new file: sprites/menu/menuItem.java
new file: sprites/menu/menuItemExclusiveMoveOnInput.java
new file: sprites/menu/menuItemLevelDecrease.java
new file: sprites/menu/menuItemLevelIncrease.java
new file: sprites/menu/menuItemMovementDirections.java
new file: sprites/menu/menuItemProjectileLimit.java
new file: sprites/menu/menuItemStartGame.java
new file: sprites/pickup/fireRateBoost.java
new file: sprites/pickup/pickup.java
new file: sprites/pickup/shield.java
new file: sprites/pickup/shieldPickup.java
new file: sprites/pickup/speedBoost.java
new file: sprites/player.java
new file: sprites/projectile.java
new file: sprites/wall.java
[user@lj src]$ git commit
[master (root-commit) 4cf5218]
Initial commit
Changes to be committed:
new file: engine/collisionChecker.java
new file: engine/direction.java
new file: engine/gameEngine.java
new file: engine/gameObjects.java
new file: engine/level.java
new file: engine/main.java
new file: engine/mathVector.java
new file: graphics/drawer.java
new file: sprites/baseSprite.java
new file: sprites/boss.java
new file: sprites/enemy.java
new file: sprites/healthBar.java
new file: sprites/menu/menuItem.java
new file: sprites/menu/menuItemExclusiveMoveOnInput.java
new file: sprites/menu/menuItemLevelDecrease.java
new file: sprites/menu/menuItemLevelIncrease.java
new file: sprites/menu/menuItemMovementDirections.java
new file: sprites/menu/menuItemProjectileLimit.java
new file: sprites/menu/menuItemStartGame.java
new file: sprites/pickup/fireRateBoost.java
new file: sprites/pickup/pickup.java
new file: sprites/pickup/shield.java
new file: sprites/pickup/shieldPickup.java
new file: sprites/pickup/speedBoost.java
new file: sprites/player.java
new file: sprites/projectile.java
new file: sprites/wall.java
27 files changed, 2557 insertions(+)
create mode 100755 engine/collisionChecker.java
create mode 100755 engine/direction.java
create mode 100755 engine/gameEngine.java
create mode 100755 engine/gameObjects.java
create mode 100755 engine/level.java
create mode 100755 engine/main.java
create mode 100755 engine/mathVector.java
create mode 100755 graphics/drawer.java
create mode 100755 sprites/baseSprite.java
create mode 100755 sprites/boss.java
create mode 100755 sprites/enemy.java
create mode 100755 sprites/healthBar.java
create mode 100755 sprites/menu/menuItem.java
create mode 100755 sprites/menu/menuItemExclusiveMoveOnInput.java
create mode 100755 sprites/menu/menuItemLevelDecrease.java
create mode 100755 sprites/menu/menuItemLevelIncrease.java
create mode 100755 sprites/menu/menuItemMovementDirections.java
create mode 100755 sprites/menu/menuItemProjectileLimit.java
create mode 100755 sprites/menu/menuItemStartGame.java
create mode 100755 sprites/pickup/fireRateBoost.java
create mode 100755 sprites/pickup/pickup.java
create mode 100755 sprites/pickup/shield.java
create mode 100755 sprites/pickup/shieldPickup.java
create mode 100755 sprites/pickup/speedBoost.java
create mode 100755 sprites/player.java
create mode 100755 sprites/projectile.java
create mode 100755 sprites/wall.java
Files are removed from the index in the same *NIX style:
[user@lj src]$ git rm -r .
rm 'engine/collisionChecker.java'
rm 'engine/direction.java'
rm 'engine/gameEngine.java'
... SNIP ...
To compare my local index with the repository, I use git diff
(note: the row of hyphens at the end of most lines was trimmed
for readability):
[user@lj src]$ git diff --cached --stat
engine/collisionChecker.java | 281 ----- ...
engine/direction.java | 14 ----
engine/gameEngine.java | 504 ----- ...
engine/gameObjects.java | 61 ----- ...
engine/level.java | 134 ----- ...
engine/main.java | 51 ----- ...
engine/mathVector.java | 46 ----- ...
graphics/drawer.java | 323 ----- ...
sprites/baseSprite.java | 303 ----- ...
sprites/boss.java | 73 ----- ...
sprites/enemy.java | 119 ----- ...
sprites/healthBar.java | 64 ----- ...
sprites/menu/menuItem.java | 21 ----- ...
sprites/menu/menuItemExclusiveMoveOnInput.java | 31 ----- ...
sprites/menu/menuItemLevelDecrease.java | 28 ----- ...
sprites/menu/menuItemLevelIncrease.java | 27 ----- ...
sprites/menu/menuItemMovementDirections.java | 30 ----- ...
sprites/menu/menuItemProjectileLimit.java | 31 ----- ...
sprites/menu/menuItemStartGame.java | 33 ----- ...
sprites/pickup/fireRateBoost.java | 39 ----- ...
sprites/pickup/pickup.java | 77 ----- ...
sprites/pickup/shield.java | 39 ----- ...
sprites/pickup/shieldPickup.java | 47 ----- ...
sprites/pickup/speedBoost.java | 38 ----- ...
sprites/player.java | 64 ----- ...
sprites/projectile.java | 44 ----- ...
sprites/wall.java | 35 ----- ...
27 files changed, 2557 deletions(-)
When used without the cached
option, only changes that have been made
without being added to the index will be displayed. The stat
option
provides a quick table instead of the standard line-by-line review
provided through the less
command without it. This is often useful when
looking for a summary of many files instead of analyzing small changes.
git status
provides a more verbose output similar to diff
:
[user@lj src]$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
deleted: engine/collisionChecker.java
deleted: engine/direction.java
deleted: engine/gameEngine.java
... SNIP ...
Branches are an essential part of git, and understanding them is paramount to grasping how git truly operates. Each branch is a different development path, which is to say that all branches are independent of one another. To explain this better, let's look at an example.
All projects start with the "master" branch. You can view the current branches with:
[user@lj src]$ git branch -a
* master
New branches are used to develop features that then can be merged back into the master branch. This way, different modules can remain separate until they are stable and ready to merge with the master branch, from which all other branches should derive. In this way, the master branch is more like a tree trunk than a branch. Creating a new branch often is referred to as "forking" the development tree, splitting the linear development into two before merging the branches again once development on the forked branch is complete.
To create a new branch, with all the data of the current branch, use:
[user@lj src]$ git checkout -b development
Switched to a new branch 'development'
checkout
is used to finalize all operations on the current branch before
detaching from it. The -b
switch creates a new branch from the current
branch. In this instance, I have created a development branch that will
contain "hot" or unstable code.
Next, rename the master branch to "stable":
[user@lj src]$ git branch -m master stable
Now that you have your two branches set up, let's compare them to ensure that the development branch has been populated with the stable branch's data:
[user@lj src]$ git diff --stat development stable
[user@lj src]$
Since diff
hasn't returned anything, you know they contain exactly the same data.
Now that you've got your two branches set up, let's begin making changes on the development branch:
[user@lj src]$ git diff --cached
diff --git a/engine/main.java b/engine/main.java
index 38577b5..5900d80 100755
--- a/engine/main.java
+++ b/engine/main.java
@@ -11,6 +11,8 @@ import graphics.drawer;
public class main
{
+ // WHEN CAN I GO BACK TO C!?!?!?
+
/*
* Class: main
* Author: Patrick
And commit them to the branch:
[user@lj src]$ git commit
[development e1f13bd] Changes to be committed: modified:
↪engine/main.java
1 file changed, 2 insertions(+)
After committing the change, you're given a unique identifier—"e1f13bd" in this case. It's used to refer to this commit; however, it's pretty hard for feeble human minds to remember something like that, so let's tag it with something more memorable.
First, find and tag the initial commit:
[user@lj src]$ git log
commit e1f13bde0bbe3d64f563f1abb30d4393dd9bd8d9
Author: user <user@lj.linux>
Date: Wed Jun 6 15:51:18 2018 +0100
Changes to be committed:
modified: engine/main.java
commit 4cf52187829c935dac40ad4b65f02c9fb6dab7ba
Author: user <user@lj.linux>
Date: Tue Jun 5 21:31:14 2018 +0100
Initial commit
Changes to be committed:
new file: engine/collisionChecker.java
new file: engine/direction.java
new file: engine/gameEngine.java
... SNIP ...
[user@lj src]$ git tag v0.1 4cf52187829c935dac40ad4b65f02c9fb
↪6dab7ba
[user@lj src]$ git tag v0.11 e1f13bd
Now that the two commits have been tagged with a version number, they're much easier to remember and compare:
[user@lj src]$ git diff --stat v0.1 v0.11
engine/main.java | 2 ++
1 file changed, 2 insertions(+)
After testing and ensuring that the development branch is stable, you should update the stable branch to the current development branch:
[user@lj src]$ git checkout stable
Switched to branch 'stable'
[user@lj src]$ git merge development
Updating 4cf5218..e1f13bd
Fast-forward
engine/main.java | 2 ++
1 file changed, 2 insertions(+)
As you can see below, both branches still exist and contain the same data:
[user@lj src]$ git branch -a
development
* stable
[user@lj src]$ git diff --stat stable development
If at any time you want to roll back, use git revert
to revert a commit and undo any changes it made:
[user@lj src]$ git revert v0.11
[stable 199169f] Revert " Changes to be committed:"
1 file changed, 2 deletions(-)
[user@lj src]$ git diff --stat stable development
engine/main.java | 2 ++
1 file changed, 2 insertions(+)
This is all you need to know to get started and become proficient in using git for personal use. All git repos operate the same way; however, working with remote repositories owned by others brings its own caveats. Read on.
First things first, let's get the remote repo:
[user2@lj ~]$ git clone src repo
Cloning into 'repo'...
done.
After some changes by user2, they are committed to user2's repo:
[user2@lj repo]$ git commit -a
[stable 6a04336] Changes to be committed: modified:
↪graphics/drawer.java
1 file changed, 1 insertion(+)
Now the original owner can pull the changes back into the main repo. Pulling fetches and merges the changes in one command where possible:
[user@lj src]$ git pull /home/user2/repo
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 4 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (4/4), done.
From /home/user2/repo
* branch HEAD -> FETCH_HEAD
Updating 199169f..6a04336
Fast-forward
graphics/drawer.java | 1 +
1 file changed, 1 insertion(+)
[user@lj src]$ git diff --cached
All changes have been pulled into the main repo, and both parties have the most up-to-date version of the code. This works exactly the same on one system as it does on a hosted site, such as GitHub. The only difference is that the repo is located by URL rather than system path.
This is just the beginning of git! This quick start guide should be sufficient for any aspiring developer or frustrated coder sick of manually rolling back versions. Now you can call yourself a gitter, or git for short!
Patrick Whelan is a first-year student at Edge Hill university in the UK. He is an aspiring developer, blogger and all-round hacker.