Last time we looked at what Git is and got a basic understanding of how it works. In this post, we'll look at actually using Git and some tips and tricks for streamlining how we use it.
The first step however, is to install Git. We can get the Windows, Mac or Linux client from the download page, or if we're using a Debian based Linux distro just install it via apt:
sudo apt-get install git
Git is primarily a Linux tool used on the command line, so the Windows option actually installs a lightweight Cygwin-like bash terminal from which we can issue commands, while also offering the option to use the Git commands from a Windows CMD or Powershell.
I cannot recommend enough prioritising the command line for using Git, we'll end up far more efficient and capable with the entire toolsuite at our fingertips. Saying that, there are few free GUI tools for Git, which can help in visualising the branches, such as Sourcetree or GitKraken, both of which are very good.
Basic Git Usage
Creating a repository
To create a brand new repository use:
This creates a .git folder in the current directory (which is where Git manages everything) and creates the master branch. From here we're good to start adding and committing files.
Alternatively, if know we want to backup our Git repository, we can create an empty remote and clone it down. This will just contain the .git folder but will have the remote references already set up. Github and Bitbucket both allow us to create free repositories online, but while Github is the more popular, Bitbucket allows us to create free private repositories.
Cloning a repository
If we already have a repository or want to clone someone else's, get the repository URL link and just run:
git clone <repositoryURL>
For example, if we wanted to clone https://github.com/robjbone/KaliScripts then
git clone https://github.com/robjbone/KaliScripts
will clone the repository into a new KaliScripts folder in the current directory. Alternatively, if we want to specify the directory (useful when scripting or we want to change the name of the folder) we can run for example:
git clone https://github.com/robjbone/KaliScripts /opt/kaliscripts
which will clone the files into /opt/kaliscripts.
The remote repository will automatically be set up as the default remote, origin.
Once we have added some files we can create a snapshot of the current status by creating a commit.
First, we run:
which will tell show us which files have been added, deleted or modified and so on. It will also detail the status of the repository relative to the remote repository, telling us if we're ahead or behind its last known reference to the remote repository.
Once we're ready we have to stage the files we want to commit. This is where we tell Git which changes we want to make up the commit. We can add individual files or folders:
git add myfile.txt git add directory/ git add directory2/*.c
or if we want to just add everything:
git add . # for everything in the working directory (recursively) git add --all # for everything in the repository
If we want to view the unstaged changes since the last commit, we can run:
or to view only the staged changes:
git diff --staged
Once we're ready to commit:
git commit -m "A descriptive commit message"
This will commit only the staged changes, so any other changed files will remain uncommitted.
When we've made our commit we'll get a hash that is the ID for our commit. This identifier is how we refer to the commit, for example when we want to review it:
git log 3b2da94
or if we want to reset it to it and so on (we'll cover this later).
We covered last time that branches are essentially just tags on commits, so creating and manipulating branches is usually very lightweight and quick.
Branches are great for just spinning off to try out something new, or if we want to make a change but we're not sure if it will work. If things work out, we can merge our branch back into the main "trunk" of commits, which is the branch called master. If things don't work out, we can just scrap our branch and our main codeline remains untouched.
If we are currently on master we can create a new branch by issuing:
git branch <branchName>
To switch to, or checkout, a branch:
git checkout <branchName>
and we can also combine the last two steps with:
git checkout -b <branchName>
Once we are on our new branch, we can just commit as do normally. We can switch back and forth between our branches by just checking those branches out.
If we like the changes we make on our branch, we can merge them back into master.
When merging branches we checkout the branch we want to change, in this case master:
git checkout master
then merge in the branch with the changes we want:
git merge <branchName>
If there are changes on both branches that conflict (such as both branches have edited the same line) then we will get merge conflicts. We'll leave this topic for a later article as it usually only crops up if two or more people are collaborating on the repository, but if it crops up in the mean time you can look at this link.
Once this is done, if we have no more use for our merged branch, as its now merged into master, we can delete it with:
git branch -d <branchName>
This will show a warning message and fail if the branch has changes that have not yet been merged to master to help us avoid accidentally deleting work.
If we instead decide that our changes on our branch are useless and should be discarded, we can delete the branch without merging with:
git branch -D <branchName>
Note that we can branch off of any branch, and merge any two branches, it's not just relative to master.
Finally, we can list all branches with:
git branch -a
Updating a repository
If the remote repository gets updated, we can pull down those changes to our local copy using:
This will grab the changes in the remote version and merge them with our local repository, updating the local branches as it does so. If we have made no changes locally to the repository it will just "fast-forward" to the new version being pulled down instead of merging (as there are no changes to merge).
If we just want to fetch the latest data without actually updating any of our local branches, we can instead run:
Now when we run
git status it will tell us how we compare to an up-to-date reference of the remote repository.
If we want to backup or share our changes we push our changes to a remote repository. This will push the latest commits on our branches, but will not push uncommitted files.
To push the current branch's changes we can use:
or to push a different branch:
git push <remoteName> <branchName>
git push origin master
Note that if the remote branch is ahead of our local one, we'll have to update our local one first. We can only push branches that are ahead of their remote counterparts.
Some useful tips and tricks for using Git are below.
We can customise just about everything in Git. We can do this at a global level, in our ~/.gitconfig file, or have repository specific configuration in a .git/config file in the repository. As is usually the case, the more specific config at the repository level overrides the global config if a conflict arises.
One thing we always want to setup is our name and email, as this affects the author and so on in our commits. We can also change the default editor that is used when editing commits, the line-endings style that's used, add aliases for commands and much more.
An example ~/.gitconfig might be:
[user] name = m0rv4i email = [email protected] [core] excludesfile = ~/.gitignore editor = vim eol = lf [push] default = simple [branch] autosetuprebase = always [alias] s = status aa = add --all
You can read more about the various options here.
Another useful configuration file is the .gitignore file. This file dictates files that will be ignored by Git. For example, we rarely want to commit log files, compiled binaries and so on. We can set a global excludes file in our config file, and also have the option to add a per-repository .gitignore file in the root of each repository (note this is NOT in the .git folder this time!)
A sample .gitignore file might be:
# Generated Binaries # ###################### *.class *.com *.dll *.exe *.o *.so *.bin *.pdb # Packages # ############ *.7z *.dmg *.gz *.iso *.rar *.tar *.zip # Logs and Databases # ###################### *.log *.sql *.sqlite
If later on we want to add a particular ignored file, we can forcibly stage it with
git add -f <ignoredFile>
If we want to undo local changes to a file and just reset it to the last commit, we can run:
git checkout -- <fileName>
If we want to amend a commit, such as to add files or change the message, stage the changes if required then run:
git commit --amend
If we want to quickly just stash our current uncommitted changes for later we can run:
and then do what we need to do. Any further stashes will be pushed onto a stack of stashes. Later when we want to re-apply those we can run:
git stash pop
which will apply the last stash and remove it from the stack.
To view the last 10 commits on the current branch run:
git log -10
If everything goes to pot and we want to just reset our current branch to a previous commit, we can run:
git reset --hard <commitHash>
This will drop all changes, staged and unstaged and just revert to the state of the provided commit.
We can also do a soft reset, where the staged and unstaged changes are not lost and only the branch reference is reset to the given hash. If we backtrack multiple commits then any changes in those commits will be instead staged, as they are no longer committed for the current branch.
git reset --soft <commitHash>
For those "oh crap" moments
Sometimes we make mistakes, but it's really hard to completely lose a commit in Git, even after merges and branch deletions and so on.
If we're totally stuck and have deleted a branch we didn't mean to or something similar, we can run
as in "reference log". This command just logs the history of what we've been doing and associated messages:
The most recent changes are are the top.
We can see in this case that even though I had deleted the test branch, the hash is still available at b28267f. Immediately after this I had checked out master (the top message) and then deleted the test branch (the deletion is not shown as it's not a change to the current reference), but we can get back to the test branch by checking out the hash:
git checkout b28267f
and creating a branch from the current state again:
git checkout -b test
or alternatively, resetting the current branch to that position again:
git reset --hard b28267f
If you're like, getting really into Git, some further reading for advanced topics are below:
Rebasing is a method of merging two branches which is possible as commits are just diffs. Instead of smushing the branches together to merge them, it takes the changes on the branch we want to merge in and "replays" the diffs onto the top of the branch being merged into. This strategy results in a super clean and easy to follow Git history as it appears to just be linear, so a real plus if you use a lot of branches or are colloborating with other people on a project.
References faciliate how Git works internally, including how Git tracks the differences between remote and local versions of branches. Understanding how these work can add some real Git-fu to your day.