DevOps (and of course DevSecOps) are getting more and more adopted by companies: these methodologies rely on several frameworks and software skills, and working with a modern Source Code Management (SCM) such any kind of software implementing Git is certainly a must for every DevOps professional. This post is the first of a set of posts dedicated to Git and is aimed at providing a GIT Tutorial - A thorough Version Control with Git Howto on personal repositories, giving guidelines to proficient operating with it.
Acquainting To Git
Git is a modern SCM tool that superseded Subversion (SVN) and the very old Concurrent Versions System (CVS): it is far more efficient and resilient, and one of its most appreciated features is certainly that it is a Distributed Version Control System.
It has been designed by Linus Torvalds himself, inspired by BitKeeper, ... this is one of the most interesting pages of the free software history: Linux Torvalds tought that, because of the growth of the project, the VCS that they used to develop the Linux kernel was not more suitable. He searched a lot and found that BitKeeper was the only suitable VCS - actually Larry Mc Voy, the creator of BitKeeper, specifically designed it in the hope to meet Torvalds's needs. In addition to that, since they also offered it for free to open-source projects, the kernel developers adopted it in 2002. They used it for almost three years, until Andrew Tridgell - the father of Samba - started to write an open source client of BitKeeper. There were other "incidents” with the open source community in the past, but this apparently was the last straw: BitMover rescinded the offer of free usage of BitKeeper for open source projects. Despite Torvalds's lashed out at Tridgell after this incident, this was not really completely bad for the open source world, since this pushed Torvalds to design and develop a great open source VCS: Git.
Simply put, working with GIT more or less consists into the following list of actions:
- pull the most current copy from the remote repository
- merge the pulled copy with the local one
- optionally check the status of files in the local repository
- add the local files modified since the last commit to the versioned files list
- commit the modify
- push to the shared repository
These result in a list of commands such as:
git pull
git status
git add --all
git commit -m "a meaningful description of these modifications"
git push
The above list of commands covers the very basic usage of Git.
Git Concepts
A Git repository is a directory tree whose contents are under the control of the GIT version management system.
Areas
This repository has three areas:
in this area there are:
- new contents
- contents modified since the last commit or since they have been put into the Staging Area
- contents deleted since the last commit or since they have been put into the Staging Area
You can see its contents by typing:
git status
they are listed beneath Untracked files.
in this area there are contents that were on the Working Area and that have been explicitly set to be tracked using the "git add" command.
Simply put, the Staging Area is the area where there are the contents ready to be committed.
You can see this contents by typing:
git status
they are listed beneath Changes to be committed.
in this area there are only contents tracked by Git that have already been committed: committing is actually the action of permanently storing contents into the Repository Area. At each commit Git computes the hash of the modified contents and uses it as an identifier to locate it inside the log of the changes.
Note that the GIT project has decided to replace SHA1 with SHA256, although the current version is still using SHA1.
You can see the log of the commits of the current checked-out branch by typing:
git log
Branches
A branch basically is a name used as an identifier to a list of one or more hashes: whenever you initialize a new GIT repository a default branch, called master for historical reasons, is automatically created.
At the first commit, Git creates a file with the name of the branch beneath the ".git/refs/heads" directory, and use it to store the hash of the committed contents: each time you commit something, the contents of .git/refs/heads/master is replaced with the new hash. So, when using the legacy name "master" for the default branch, it creates the .git/refs/heads/master file.
TAGS
Tags are names used to conveniently identify one and only one hash: their purpose is to provide a human-readable name to refer to a hash so as to ease checking out contents of a given point of the story.
Install Git
Install Git is really simple: if you are using a dnf based Linux distribution, such as Red Hat Enterprise Linux 8 or above, just by type:
sudo dnf install -y git
otherwise, if you are using an older Red Hat Enterprise Linux version use yum as follows:
sudo yum install -y git
Configure Git
Git stores its global per user configuration beneath the ~.gitconfig file: this is an ini file structured using multiple stanzas: you can edit it directly with an editor or type "git config" statements that store the settings into it – the advantage of using this last method is that the command line validates the syntax, avoiding to store non-existent directives inside the configuration file.
The initial thing every user must do is set up his name and surname along with his email: these are used to identify him as the committer of each modification he does to any file in the repository.
For example, to set them up issue:
git config --global user.name 'Marco Carcano'
git config --global user.email mcarcano@carcano.local
Please note the --global parameter: this marks these settings as global, that means that they are stored into the ~.gitconfig file and so they are applied to any repository you will work on.
If you are not in a production environment (or more precisely, if you do not have security concerns) and the certificate of the GIT repository is self-signed, you can configure GIT not to verify the certificate - just type:
git config --global sslVerify = false
Another handy setting is defining the default push method - my personal advice is to set it to "simple":
git config --global push.default simple
The behavior for “simple” value is to push the current branch to the corresponding upstream branch (that by the way is a best practice too). Anyway, mind that as a safety measure, the push is aborted if the upstream branch does not have the same name as the local one.
Working With Git Repositories
There are only two possible scenarios when working with Git: create a new repository or work with an existing one - that is called "clone" an existing Git repository.
Creating A New Repository
The statement to create either a personal or a shared repository is "git init" followed by the name of the repository you want to create.
Personal Git Repository
Creating an empty git repository is as easy as typing "git init" followed by the name or the repository. For example, to create the "waldo" empty repository, just type:
git init waldo
sometimes instead you have existing data that you want to version control using Git. As an example, let's create the "foo" directory and the "Readme.md" file inside it:
mkdir ~/foo
cd ~/foo
echo "# README" > Readme.md
we can easily "turn" this directory along with its contents into a Git repository by typing the following command inside the "foo" directory itself:
git init
when creating the repository, git creates the .git directory to store its own metadata inside the directory that must be the root of the directory tree of versioned files.
We can now add the "Readme.md" file to the Staging Area:
git add Readme.md
and eventually commit the contents of the Staging Area into the Repository Area specifying "initial commit" as the commit message:
git commit -m "initial commit"
Shared (bare) Git Repository (Remote)
The straightforward limitation of a personal Git repository is that you cannot share it with other people. To overcome this, you can create a shared bare repository on a server that can be accessed also from other users. Once created, you can either clone it only or link it as the remote of one or more personal Git repositories.
In this example, we set up a shared Git repository that can be accessed using SSH. The very first thing to do is logging-in as a sudo enabled user to the server that will hosts the repository ("scm.carcano.local" in this example) and create the account for the git user:
sudo adduser git
sudo passwd git
Now connect to any of the client hosts you want to use - if the user you are logged as on the client doesn't have an SSH key-pair, please generate it using the "ssh-keygen" command line utility.
Now authorize the SSH key of the current user to login as "git" user to the "scm.carcano.local" server as follows:
ssh-copy-id git@scm.carcano.local
of course accept the fingerprint of the host key if necessary - it is enough to type "yes", then type the password you have just assigned to the "git" user.
The command is "git init --bare --shared=true" followed by the name of the repository you want to create.
Now that the "git" user has been created, and our SSH key has been authorized, we are finally ready to create the shared Git project.
In this example we create the "foo.git" Git repository executing the statement as a remote SSH command:
ssh git@scm.carcano.local git init --bare --shared=true foo.git
the outcome is as follows:
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
Initialized empty shared Git repository in /home/git/foo.git/
as the messages suggest, the new repository is obviously empty.
Linking The Personal Git Repository To A Shared Remote
So far we have:
- a shared remote bare Git repository on the "scm.carcano.local" server
- a personal Git repository on the local client
The next step is to link the personal Git repository to the shared remote one.
Let's change directory to the "foo" local repository:
cd ~/foo
we can now link this personal Git repository to the remote shared Git repository we have just created; this is achieved by configuring a named link (the standard name is "origin") to the remote shared Git repository. Just type the following command:
git remote add origin ssh://git@scm.carcano.local:/home/git/foo.git
Set The Upstream Of The Current Branch
We must now set a default upstream link for each of the branches of the local repository. We can list the available local branches typing:
git branch
the outcome is as follows:
* master
so we have just one branch, called "master". We can now set the link identified by the name "origin" as the default upstream remote repository for the "master" local branch:
git push -u origin master
the outcome is as follows:
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Writing objects: 100% (3/3), 220 bytes | 220.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To ssh://localhost:/home/git/foo.git
* [new branch] master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.
the command, besides configuring the upstream link, also performs the initial push. Git by default pushes a local branch to a branch with the same name on the remote shared repository. This means that now the "master" branch of the local personal Git repository and "master" branch of the remote shared repository have the same contents.
Cloning An Existing Repository
As we saw, the mandatory condition to operate with an existing shared repository is to create a clone of it: the statement to use is "git clone" followed by
- the URL of the git repository you want to clone
- optionally, the name of the directory you want to use as the destination of the clone
There are several kinds of protocols that can be used to give access to the repository (provided the server hosting it supports them), for example:
URL of a repository stored into the local filesystem - it is the default protocol
URL of a repository stored on the filesystem of a server that can be reached via SSH
URL of a repository that can be reached by using HTTP or HTTPS protocols
As an example, remove the "foo" personal repository we created on the client host:
rm - rf foo
we can now get it back simply by cloning it:
git clone git@scm.carcano.local:/home/git/foo.git ~/foo
Version Control Operations
Now we have a personal local repository linked to a remote shared bare repository: this means that we can now start doing something.
Populating The Working Area
This is the most straightforward thing to do: just add files and directories to the current personal Git repository, either creating or copying them from elsewhere.
For example:
cd ~/foo
mkdir -p bar/baz
touch xyzzy.txt bar/baz/qux.txt bar/baz/plugh.txt
mkdir fred
cp -dR /usr/share/doc/git/howto .
now that we have added some contents, we can see how they are seen by the Git repository perspective:
git status
the outcome is as follows:
On branch master
Your branch is up to date with 'origin/master'.
Untracked files:
(use "git add ..." to include in what will be committed)
bar/
howto/
xyzzy.txt
nothing added to commit but untracked files present (use "git add" to track)
as expected the "bar" and "howto" directories are listed as ''Untracked files" - and so they are correctly in the Working Area.
Staging Modified Contents
Let's now add the contents to the Staging Area.
The command to use is "git add" followed by the path of the contents we want to stage, so for example:
git add bar/baz/qux.txt
stages just the "bar/baz/qux.txt" file: if we run "git status" we get the following outcome indeed:
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git restore --staged ..." to unstage)
new file: bar/baz/qux.txt
Untracked files:
(use "git add ..." to include in what will be committed)
bar/baz/plugh.txt
howto/
xyzzy.txt
you can of course stage a whole directory tree - for example, to stage the whole "howto" directory tree:
git add howto
if we run "git status" this time the outcome is as follows:
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git restore --staged ..." to unstage)
new file: bar/baz/qux.txt
new file: howto/keep-canonical-history-correct.html
new file: howto/keep-canonical-history-correct.txt
new file: howto/maintain-git.html
new file: howto/maintain-git.txt
new file: howto/new-command.html
new file: howto/new-command.txt
new file: howto/rebase-from-internal-branch.html
new file: howto/rebase-from-internal-branch.txt
new file: howto/rebuild-from-update-hook.html
new file: howto/rebuild-from-update-hook.txt
new file: howto/recover-corrupted-blob-object.html
new file: howto/recover-corrupted-blob-object.txt
new file: howto/recover-corrupted-object-harder.html
new file: howto/recover-corrupted-object-harder.txt
new file: howto/revert-a-faulty-merge.html
new file: howto/revert-a-faulty-merge.txt
new file: howto/revert-branch-rebase.html
new file: howto/revert-branch-rebase.txt
new file: howto/separating-topic-branches.html
new file: howto/separating-topic-branches.txt
new file: howto/setup-git-server-over-http.html
new file: howto/setup-git-server-over-http.txt
new file: howto/update-hook-example.html
new file: howto/update-hook-example.txt
new file: howto/use-git-daemon.html
new file: howto/use-git-daemon.txt
new file: howto/using-merge-subtree.html
new file: howto/using-merge-subtree.txt
new file: howto/using-signed-tag-in-pull-request.html
new file: howto/using-signed-tag-in-pull-request.txt
Untracked files:
(use "git add ..." to include in what will be committed)
bar/baz/plugh.txt
xyzzy.txt
as you see not there are only "xyzzy.txt" and "bar/baz/plugh.txt" files still in the Working Area
We can stage all the files that are in the Working Area as a whole simply specify the "--all" parameter.
So, to add the remaining files just type:
git add --all
if we run "git status" again:
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git restore --staged ..." to unstage)
new file: bar/baz/plugh.txt
new file: bar/baz/qux.txt
new file: xyzzy.txt
new file: howto/keep-canonical-history-correct.html
new file: howto/keep-canonical-history-correct.txt
new file: howto/maintain-git.html
new file: howto/maintain-git.txt
new file: howto/new-command.html
new file: howto/new-command.txt
new file: howto/rebase-from-internal-branch.html
new file: howto/rebase-from-internal-branch.txt
new file: howto/rebuild-from-update-hook.html
new file: howto/rebuild-from-update-hook.txt
new file: howto/recover-corrupted-blob-object.html
new file: howto/recover-corrupted-blob-object.txt
new file: howto/recover-corrupted-object-harder.html
new file: howto/recover-corrupted-object-harder.txt
new file: howto/revert-a-faulty-merge.html
new file: howto/revert-a-faulty-merge.txt
new file: howto/revert-branch-rebase.html
new file: howto/revert-branch-rebase.txt
new file: howto/separating-topic-branches.html
new file: howto/separating-topic-branches.txt
new file: howto/setup-git-server-over-http.html
new file: howto/setup-git-server-over-http.txt
new file: howto/update-hook-example.html
new file: howto/update-hook-example.txt
new file: howto/use-git-daemon.html
new file: howto/use-git-daemon.txt
new file: howto/using-merge-subtree.html
new file: howto/using-merge-subtree.txt
new file: howto/using-signed-tag-in-pull-request.html
new file: howto/using-signed-tag-in-pull-request.txt
this time the Working Area is empty, and all files are now in the Staging Area.
Mind that we are staging the current version of the contents: this means that if we change anything after staging something we will find two different versions of these contents together: the one in the Staging Area and the new contents in the Working Area.
As an example, let's modify the contents of the "xyzzy.txt" file:
echo '"Xyzzy" is a magic word that teleports the player between two locations ("inside building" and the "debris room").' > xyzzy.txt
let's run "git status" once again:
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git restore --staged ..." to unstage)
new file: bar/baz/plugh.txt
new file: bar/baz/qux.txt
new file: howto/keep-canonical-history-correct.html
new file: howto/keep-canonical-history-correct.txt
new file: howto/maintain-git.html
new file: howto/maintain-git.txt
new file: howto/new-command.html
new file: howto/new-command.txt
new file: howto/rebase-from-internal-branch.html
new file: howto/rebase-from-internal-branch.txt
new file: howto/rebuild-from-update-hook.html
new file: howto/rebuild-from-update-hook.txt
new file: howto/recover-corrupted-blob-object.html
new file: howto/recover-corrupted-blob-object.txt
new file: howto/recover-corrupted-object-harder.html
new file: howto/recover-corrupted-object-harder.txt
new file: howto/revert-a-faulty-merge.html
new file: howto/revert-a-faulty-merge.txt
new file: howto/revert-branch-rebase.html
new file: howto/revert-branch-rebase.txt
new file: howto/separating-topic-branches.html
new file: howto/separating-topic-branches.txt
new file: howto/setup-git-server-over-http.html
new file: howto/setup-git-server-over-http.txt
new file: howto/update-hook-example.html
new file: howto/update-hook-example.txt
new file: howto/use-git-daemon.html
new file: howto/use-git-daemon.txt
new file: howto/using-merge-subtree.html
new file: howto/using-merge-subtree.txt
new file: howto/using-signed-tag-in-pull-request.html
new file: howto/using-signed-tag-in-pull-request.txt
new file: xyzzy.txt
Changes not staged for commit:
(use "git add ..." to update what will be committed)
(use "git restore ..." to discard changes in working directory)
modified: xyzzy.txt
as you see now "xyzzy.txt" is shown twice: we have the staged version (the empty file we initially created using the touch command) and a new version, with contents that explain the meaning of the word "xyzzy".
Let's add these precious information to the Staging Area - that of course means overwriting the contents of "xyzzy.txt" currently in the Staging Area:
git add xyzzy.txt
Unstaging Contents
You may of course sometimes need to remove the contents from the Staging Area (this is called unstage) by using the "git reset" command.
You can remove only specific contents from the Staging Area - for example, to just remove the "bar/baz/plugh.txt" file:
git reset HEAD bar/baz/plugh.txt
An alternate way is using the "git restore" command with the --staged option. For example, to restore the "xyzzy.txt" file type:
git restore --staged xyzzy.txt
let's check the outcome running "git status":
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git restore --staged ..." to unstage)
new file: bar/baz/qux.txt
new file: howto/keep-canonical-history-correct.html
new file: howto/keep-canonical-history-correct.txt
new file: howto/maintain-git.html
new file: howto/maintain-git.txt
new file: howto/new-command.html
new file: howto/new-command.txt
new file: howto/rebase-from-internal-branch.html
new file: howto/rebase-from-internal-branch.txt
new file: howto/rebuild-from-update-hook.html
new file: howto/rebuild-from-update-hook.txt
new file: howto/recover-corrupted-blob-object.html
new file: howto/recover-corrupted-blob-object.txt
new file: howto/recover-corrupted-object-harder.html
new file: howto/recover-corrupted-object-harder.txt
new file: howto/revert-a-faulty-merge.html
new file: howto/revert-a-faulty-merge.txt
new file: howto/revert-branch-rebase.html
new file: howto/revert-branch-rebase.txt
new file: howto/separating-topic-branches.html
new file: howto/separating-topic-branches.txt
new file: howto/setup-git-server-over-http.html
new file: howto/setup-git-server-over-http.txt
new file: howto/update-hook-example.html
new file: howto/update-hook-example.txt
new file: howto/use-git-daemon.html
new file: howto/use-git-daemon.txt
new file: howto/using-merge-subtree.html
new file: howto/using-merge-subtree.txt
new file: howto/using-signed-tag-in-pull-request.html
new file: howto/using-signed-tag-in-pull-request.txt
Untracked files:
(use "git add ..." to include in what will be committed)
bar/baz/plugh.txt
xyzzy.txt
If instead you want to restore everything back from the Staging Area to the Working Area:
git reset
we'll see more "git reset" examples very soon.
Committing Contents
When you think that has come the time to commit the changes of the versioned contents into the Repository Area you should only type "git commit" followed by -m parameter and the comment that describes the changes you are committing.
To go on with our example, first we need add again all the contents to the Staging Area:
git add --all
and now that everything we want to commit has been staged again, we can run the following statement:
git commit -m "explained the meaning of the words plugh and xyzz"
Amending A Commit
It may happen that, after a commit, you realize that you forgot something or made a mistake in the comment - for example I typed "xyzz" instead of "xyzzy" in the comment. We can sort out my mistake as follows:
git commit --amend -m "explained the meaning of the words plugh and xyzzy"
we can of course exploit the amend feature for example if we forgot something. Let-s check the contents of the "xyzzy.txt" file.
For example we certainly forgot to explain the meaning of the "plugh" word, whereas in the comment we claimed having done it. Let's add the explain right now as follows:
echo 'In the Colossal Cave Adventure game, when the player first arrives at an area known as "Y2", he receives the message A hollow voice says "plugh". The magic word takes the player between the rooms "inside building" and "Y2".' > bar/baz/plugh.txt
now let's stage it again:
git add bar/baz/plugh.txt
and commit it again amending both the contents and the comment:
git commit --amend -m "explained the meaning of the words plugh and xyzzy"
let's have a look to the log of the commits:
git log
the output is as follows:
commit e1d6060757d59356e2f3ddc311ed5f42adc65d8f (HEAD -> master)
Author: Marco Carcano <mcarcano@carcano.local>
Date: Sat Aug 27 14:56:07 2022 +0200
explained the meaning of the words plugh and xyzzy
commit 79da3eec749e33810c7eacdb13f6af27ee88cd05 (origin/master)
Author: Marco Carcano <mcarcano@carcano.local>
Date: Thu Aug 18 23:59:04 2022 +0200
initial commit
as we see that there's just one commit with the message "explained the meaning of the words plugh and xyzzy": this is because we amended each time what we forgot.
Reverting A Specific Commit
It may happen that you want to “undo” changes of a specific commit, without affecting the contents of the other commits that have been made after it. In practice it extracts into the Working Area the contents of the repository skipping the changes of the commit specified in the statement.
This is particularly useful when you are the owner of a commit, but you have already pushed it to the remote repository - and so now there are chances that other people may potentially have started modifying it.
In such a situation you can simply type "git revert" followed by the hash of the commit you want to be skipped when extracting the files. Anyway, mind that the outcome may result in conflict changes that you have to manually solve. For example, if you want to skip just the last commit, type:
git revert HEAD
Rewind To An Earlier Point In History
Sometimes we do "commit" several times of modifications that lead to a dead end, eventually finding out that some of the last commits we did are not useful or maybe do not suit our needs or worse are even completely wrong.
Git provides the reset command to do exactly this: rewind the history to a specific commit.
Git reset can have three distinct behaviors: "--mixed", "--soft" and "--hard".
this is the default behavior: it extracts the changes from the specified commits to the Working Area: you typically use it to make additional changes (that maybe you forgot early) to the files and eventually put everything as a whole into a larger commit.
We already saw this example, used to unstage the "bar/baz/qux.txt" file:
git reset HEAD bar/baz/qux.txt
it extracts the changes from the specified commits to the Staging Area: you typically do this when you want to consolidate a group of small commits into a larger one.
it:
- resets everything in the Staging Area
- resets everything in the Working Area
- removes every commit did after the specified one from the Repository Area
The outcome is that we reach the state there was in the commit you specified.
For example, to go back to how we were three commits ago, just type:
git reset --hard HEAD~2
or specify the hash of the commit we want to revert to:
git reset --hard <hash>
Working With Branches
Very often it is needed to have multiple parallel lines of development, each one of them focusing on a particular topic of a project, that in the end gets merged into the main project: this need can be addressed by creating as many branches as many are the parallel developments.
These kinds of branches are often referred to as “short-living branches”, but there are also "long-living" branches, such as the "develop" and the "hotfix" branches used when adopting the GitFlow branching model.
Switching from a branch (and so from a line of development) to another is called checkout.
To see things in action, let's create and populate another Git repository so to have a playground to experiment on:
git init ~/bar
cd ~/bar
touch README.md
git add README.md
git commit -m "Initial commit"
Checkout A Branch And Managing Branches
As we saw, a default branch (called “master”, although now "main" is becoming more popular) is automatically created as soon as the repository is initialized.
We can however fork other branches from the “master” branch: for example we can fork a branch called “develop” to use this long-living branch as a collector of all the commits and merges of other branches. Each time we have a stable situation and we think is suitable for being permanently stored, we can merge the “develop” branch into the “master” branch.
We can easily list the existing local branches in a Git repository by typing:
git branch -l
since we have not other branches besides the "master", the output is as follows:
* master
Creating Branches
To create a new branch, just type a "git branch" followed by the name of the branch you want to create.
For example, to create "develop" branch:
git branch dev
this command creates the new head file ".git/refs/heads/dev" and stores into it the same hash of the branch you are currently working on.
To use the new branch you must then checkout to it as follows:
git checkout dev
by now until the next time you checkout a different branch, every commit you do gets stored beneath the list of commits of the "dev" branch. The hash of the last commit, called the HEAD, is stored into the ".git/refs/heads/dev" file, and gets replaced at each new commit.
Renaming A Branch
If you change your mind on the name of the current branch, you can change it to a new one by using -M option.
For example, to change the name of the current branch (the "dev" branch) into "develop" type:
git branch -M develop
Deleting A Branch
If you are working with a short-lived branch (for example a branch dedicated to developing a specific feature) you can delete it right after merging back to the parent.
To delete a branch type "git branch -d" followed by the name of the branch you want to delete.
For example, to delete the "develop", first checkout the master:
git checkout master
then type issue:
git branch -d develop
Mind that if the branch you want to delete has been partially merged because of conflicts, you must delete using the uppercase "d". So, for example, to delete the "f-feature" branch you must type "git branch -D f-foofeature".
Stashing
Checking out an existing branch requires to have both the Working Area and the Staging Area clean: if they aren't, you are very likely everything to wind up into a mess, since if their contents are still there when you want to stage other contents that make sense only in this other branch or even worse when committing to this other branch. There are only two way to clear them:
- resetting both areas, with the straightforward downside of losing everything you did since the last commit
- committing, but this may run counter to the good practice of committing only when the contents make sense overall: you may be indeed asked to immediately switch to another branch to fix something urgent when you are in the middle of developing, and so you would actually commit something somehow incomplete. This probably is not in the "true" spirit of using Git.
For this specific use case, Git provides the "stash" feature: by stashing you are actually saving into a stack the content of the Staging Area.
Let's see this in action: let's start by checking out the new "f-fantasia" branch, and then checkout the master branch again:
git checkout -b f-fantasia
git checkout master
now let's modify the contents of the "master" branch:
echo "# Readme" > README.md
git add README.md
touch master.txt
let's check the status then:
git status
we expect the "README.md" file to be in the Staging Area, while "master.txt" must still be in the Working Area.
On branch master
Changes to be committed:
(use "git restore --staged ..." to unstage)
modified: README.md
Untracked files:
(use "git add ..." to include in what will be committed)
master.txt
If for example we would checkout to the "f-fantasia" branch to modify its contents now, both the Staging Area and Working Area are unclean: operating on this other branch in such a scenario has a high risk to wind up into a mess, unless we are not very careful and most of all fully aware of what we are doing.
Stashing Contents
So it is exactly here where we do have to use the stash command: since also the Working Area is dirty, by using the "-u" command line switch we stash its contents along the one of the Staging Area as follows:
git stash -u
the output is as follows:
Saved working directory and index state WIP on master: 9658bbb Initial commit
You can now safely checkout the "f-fantasia" branch:
git checkout f-fantasia
and do whatever we wish, adding contents and committing them or even stashing again: mind that you can add contents to the stash stack as necessary, even before jumping from branch to branch.
So for example:
echo "# Fantasia" > fantasia.txt
git add fantasia.txt
git stash
this time the "-u" command line switch is not necessary since the Working Area is still clean.
Listing The Stashes In The Stack
Let's checkout to the master now:
git checkout master
we can list the available stashes in the stack by typing:
git stash list
the output is as follows:
stash@{0}: WIP on f-fantasia: 9658bbb Initial commit
stash@{1}: WIP on master: 9658bbb Initial commit
mind that:
- stash{n} is the identifier of the stash put in the stack
- stash{0} is the identifier of the last stash put in the stack
- the remaining text is a description that helps you to guess when it has been taken, and from what branch
this is obviously vital to know when you have more entries on the stack of stashes.
We can apply any stash back to the Staging Area and Working Area as follows - mind that this command does not remove the stash from the stack:
git stash apply stash@{1}
we applied the stash "stash@{1}" since we are in the "master" branch, and that stash was taken from the "master" branch indeed.
On branch master
Changes not staged for commit:
(use "git add ..." to update what will be committed)
(use "git restore ..." to discard changes in working directory)
modified: README.md
Untracked files:
(use "git add ..." to include in what will be committed)
master.txt
no changes added to commit (use "git add" and/or "git commit -a")
As you can see from the output, everything has been restored as it was initially. You can now add everything to the Staging Area and commit:
git add --all
git commit -m "learning git stash"
it may happen that you got merging conflicts while applying a stash: in this case, you can save the contents of stash to a new branch, and then merge it as usual (more on this topic later on):
git stash branch f-tmp stash@{0}
the output is as follows:
Switched to a new branch 'f-tmp'
On branch f-tmp
Changes to be committed:
(use "git restore --staged ..." to unstage)
new file: fantasia.txt
Dropped stash@{0} (3861babedb7226e65112410eae639afdc8e7c7b7)
as you see, this command does also remove the entry from the stack of the stashes:
git stash list
since we have not branches but the "master", the output is as follows:
stash@{0}: WIP on master: 9658bbb Initial commit
we can of course remove the last entry from the stack of stashes as follows:
git stash drop stash@{0}
Mind that if you want to apply the last stash, you can just type "git stash pop": in addition to apply the stash, it gets also removed from the stack of stashes.
Let's cleanup everything:
git checkout master
git branch -d f-fantasia
git reset --hard
Merging Branches
When working with branches sooner or later you will need to melt the contents of one branch into another - a typical use case of branches are feature branches: you checkout a branch dedicated to develop a new feature, and when you are done you merge it back into the branch you generated the feature branch from.
Whenever you commit to a branch, you are making it diverge from its ancestor: this means that its "head" is a certain number of commits ahead of its ancestor.
When merging you have the following subject and object:
- Target branch: the branch you checked out and you are currently in, that is the target of the merge operation indeed
- Source branch: the branch you want to melt the contents from into the target branch. Mind that the source branch does not get deleted after the merge, unless you explicitly do it
Branch Merging Strategies
Git provides several merging strategies: in this post we see in detail the most used ones: fast-forward, recursive and squash, but mind that there do exist other strategies such as the resolve strategy, the octopus strategy and so on: simply it is not possible to describe them within a single post, so I decided to describe the ones that it is more likely to use.
As usual I want to show things in action, so let's start by creating and populating another Git repository so to have a playground to experiment with:
git init ~/baz
cd ~/baz
touch README.md
git add README.md
git commit -m "Initial commit"
Fast Forward
This is the easiest to understand merging strategy - it requires that:
- both branches have a common commit, that is an ancestor of the source branch
- the ancestor is the last commit of the target branch
Unfortunately, unless you are working on local branches, this is quite a rare use case.
These are the statements you would type to merge the "source-branch" branch into the "target-branch" branch:
git checkout target-branch
git merge --ff-only source-branch
Let's see thin in action on the "bar" git repo we created as a playground:
echo "# Readme" > README.md
git add README.md
git commit -m "added the heading
let's have a look at the log:
git log --all --decorate --oneline --graph
the output is as follows:
* 8add783 (HEAD -> master) added the heading
* 86cb5d7 Initial commit
let's now create the "f-amazing" branch, modify the contents a few times committing each single change:
git checkout -b f-amazing
echo "1st change" > amazing.txt
git add amazing.txt; git commit -m "amazing - 1st commit"
echo "2nd change" >> amazing.txt
git add amazing.txt; git commit -m "amazing - 2nd commit"
echo "3rd change" >> amazing.txt
git add amazing.txt; git commit -m "amazing - 3rd commit"
let's checkout the "master" branch:
git checkout master
and have a look at the log again:
git log --all --decorate --oneline --graph
this time the output is as follows:
* 96e6c84 (f-amazing) amazing - 3rd commit
* 471bf17 amazing - 2nd commit
* 9d46249 amazing - 1st commit
* 8add783 (HEAD -> master) added the heading
* 86cb5d7 Initial commit
as you see, commits "9d46249", "471bf17" and "96e6c84" are listed into the "f-amazing" branch.
Let's merge the "f-amazing" feature branch into the current branch forcing the use of the fast-forward strategy:
git merge --ff-only f-amazing
the output is as follows:
Updating 8add783..96e6c84
Fast-forward
amazing.txt | 3 +++
1 file changed, 3 insertions(+)
create mode 100644 amazing.txt
if the development of the feature is over, we can of course delete the "f-amazing" branch:
git branch -d f-amazing
and have a last look at the log:
git log --all --decorate --oneline --graph
this time the output is as follows:
* 96e6c84 (HEAD -> master) amazing - 3rd commit
* 471bf17 amazing - 2nd commit
* 9d46249 amazing - 1st commit
* 8add783 added the heading
* 86cb5d7 Initial commit
as you see, now commits "9d46249", "471bf17" and "96e6c84" are embedded into the "master" branch, and the "f-amazing" branch has been deleted
Rebase And Fast Forward
As we saw, one of the two mandatory conditions for being able to do a fast-forward merge is that the ancestor commit of the branches must also be the last commit of the target branch. Unfortunately, this condition is only very seldom met.
Anyway, if at least both the branches have a common ancestor, if it make sense with the layout of the history you want to keep in your repository you can exploit the rebase feature to relink the source branch to the tip of the target branch:
git checkout source-branch
git rebase target-branch
then we can actually proceed with a fast-forward merge:
git checkout target-branch
git merge --ff-only source-branch
let's see in action using the "bar" repository we created as a playground.
We start from having look at the log:
git checkout master
git log --all --decorate --oneline --graph
this time the output is as follows:
* 96e6c84 (HEAD -> master) amazing - 3rd commit
* 471bf17 amazing - 2nd commit
* 9d46249 amazing - 1st commit
* 8add783 added the heading
* 86cb5d7 Initial commit
this time we create the "f-nice" branch and modify the contents a few times committing at each single change:
git checkout -b f-nice
echo "1st change" > nice.txt
git add nice.txt; git commit -m "nice - 1st commit"
echo "2nd change" >> nice.txt
git add nice.txt; git commit -m "nice - 2nd commit"
then checkout the "master" branch again, modify the contents a few times committing at each single change:
git checkout master
echo "4th change" > amazing.txt
git add amazing.txt; git commit -m "master - 1st commit"
echo "5th change" >> amazing.txt
git add amazing.txt; git commit -m "master - 2nd commit"
lastly, let's checkout the "f-nice" branch again, modify the contents and commit the change:
git checkout f-nice
echo "3rd change" > nice.txt
git add nice.txt; git commit -m "nice - 3rd commit"
we made enough changes to provide a good example for this merge, but first let's have a look to the history - let's checkout the master branch:
git checkout master
and have a look to the log again:
git log --all --decorate --oneline --graph
this time the output is as follows:
* 91f5b11 (f-nice) nice - 3rd commit
* 56189ce nice - 2nd commit
* f8657ac nice - 1st commit
| * 91a331f (HEAD -> master) master - 2nd commit
| * b10d323 master - 1st commit
|/
* 96e6c84 amazing - 3rd commit
* 471bf17 amazing - 2nd commit
* 9d46249 amazing - 1st commit
* 8add783 added the heading
* 86cb5d7 Initial commit
as you see:
- the common ancestor of the branches is the "96e6c84" commit
- "f8657ac", "56189ce" and "91f5b11" commits belong to the "f-nice" branch
- "b10d323" and "91a331f" commits belong to the "master" branch
so both branches have the "96e6c84" commit as ancestor, but they diverged.
Indeed, if we check the difference in number of commits from the "96e6c84" and the HEAD of the "master" branch:
git rev-list --left-right --count 96e6c84...master
we got:
0 2
that means that the "96e6c84" commit is 2 commits behind the head of the "master" branch.
This actually prevents us from being able to do a fast-forward merge:
git merge --ff-only f-nice
the outcome is the following error:
fatal: Not possible to fast-forward, aborting.
let's retry after rebasing "f-nice" branch on the "master" branch:
git checkout f-nice
git rebase master
checkout the "master" branch and have a look at the log again:
git checkout master
git log --all --decorate --oneline --graph
this time the output is as follows:
* 0cac194 (f-nice) nice - 3rd commit
* f7804be nice - 2nd commit
* 78400b2 nice - 1st commit
* 91a331f (HEAD -> master) master - 2nd commit
* b10d323 master - 1st commit
* 96e6c84 amazing - 3rd commit
* 471bf17 amazing - 2nd commit
* 9d46249 amazing - 1st commit
* 8add783 added the heading
* 86cb5d7 Initial commit
this way the "91a331f" commit has become the ancestor of the "f-nice" branch, and the ancestor is not behind the HEAD of the "master" branch:
git rev-list --left-right --count 91a331f...master
we got:
0 0
so let's eventually try the fast-forward merge:
git merge --ff-only f-nice
this time it works like a charm:
Updating 91a331f..0cac194
Fast-forward
nice.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 nice.txt
we can of course delete the "f-nice" branch:
git branch -d f-nice
and have a last look at the log:
git log --all --decorate --oneline --graph
this time the output is as follows:
* 0cac194 (HEAD -> master) nice - 3rd commit
* f7804be nice - 2nd commit
* 78400b2 nice - 1st commit
* 91a331f master - 2nd commit
* b10d323 master - 1st commit
* 96e6c84 amazing - 3rd commit
* 471bf17 amazing - 2nd commit
* 9d46249 amazing - 1st commit
* 8add783 added the heading
* 86cb5d7 Initial commit
Recursive Merge
The downside of the fast-forward merging strategy is that, since it turns the graph into linear, actually hides the real track of the commits: this applies also if you rebase the branch before merging obviously.
If your goal is to keep the real track of the commits, you must use the recursive merge strategy: let's see in action using the "bar" repository we created as a playground:
git checkout master
Let's checkout the "f-new" feature from the "master" branch , modify the contents a few times and commit at each single change:
git checkout -b f-new
echo "1st change" > new.txt
git add new.txt; git commit -m "new - 1st commit"
echo "2nd change" >> new.txt
git add new.txt; git commit -m "new - 2nd commit"
then checkout the "master" branch, modify the contents a few times and commit at each single change:
git checkout master
echo "6th change" > amazing.txt
git add amazing.txt; git commit -m "master - 3rd commit"
echo "7th change" >> amazing.txt
git add amazing.txt; git commit -m "master - 4th commit"
lastly, let's checkout the "f-new" branch again, modify the contents and commit them:
git checkout f-new
echo "3rd change" > new.txt
git add new.txt; git commit -m "new - 3rd commit"
let's checkout the "master" branch:
git checkout master
and have a look to the log again:
git log --all --decorate --oneline --graph
this time the output is as follows:
* 37ee1e7 (f-new) new - 3rd commit
* e4d4279 new - 2nd commit
* a68267a new - 1st commit
| * 91f5697 (HEAD -> master) master - 4th commit
| * 639af9f master - 3rd commit
|/
* 0cac194 nice - 3rd commit
* f7804be nice - 2nd commit
* 78400b2 nice - 1st commit
* 91a331f master - 2nd commit
* b10d323 master - 1st commit
* 96e6c84 amazing - 3rd commit
* 471bf17 amazing - 2nd commit
* 9d46249 amazing - 1st commit
* 8add783 added the heading
* 86cb5d7 Initial commit
that is exactly the same use case we had in the rebase and fast-forward merge:
- the common ancestor of the branches is the "0cac194" commit
- "a68267a", "e4d4279" and "37ee1e7" commits belongs to the "f-new" branch
- "639af9f" and "91f5697" commits belongs to the "master" branch
so both branches have the "0cac194" commit as ancestor.
If we check the difference in number of commits from the "0cac194" and the HEAD of the "master" branch:
git rev-list --left-right --count 0cac194...master
we got:
0 2
that means that the "0cac194" is 2 commits behind the head of the "master" branch.
In such a scenario the fast-forward strategy is not viable, so let's proceed with the recursive merge as follows:
git merge f-new
the output is:
Merge made by the 'recursive' strategy.
new.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 new.txt
as you see, the outcome of this merge is a new commit with the melted contents of the two branches.
Mind that if the common ancestor on the target branch is not behind the head of the target branch, Git will prefer the fast-forward strategy, but you can force it to use the recursive strategy by providing the "no-ff" command line switch. For example:
git merge --no-ff f-source-branch
Now we can of course delete the "f-new" branch:
git branch -d f-new
and have a last look at the log:
git log --all --decorate --oneline --graph
this time the output is as follows:
* 0fd7834 (HEAD -> master) Merge branch 'f-new'
|\
| * 37ee1e7 new - 3rd commit
| * e4d4279 new - 2nd commit
| * a68267a new - 1st commit
* | 91f5697 master - 4th commit
* | 639af9f master - 3rd commit
|/
* 0cac194 nice - 3rd commit
* f7804be nice - 2nd commit
* 78400b2 nice - 1st commit
* 91a331f master - 2nd commit
* b10d323 master - 1st commit
* 96e6c84 amazing - 3rd commit
* 471bf17 amazing - 2nd commit
* 9d46249 amazing - 1st commit
* 8add783 added the heading
* 86cb5d7 Initial commit
as you can see, now the graph clearly highlights the ancestor of the "f-new" branch, the commits that have been made inside the branch, as well as the ones made in the master branch, and eventually the commit used to merge the "f-new" branch into the "master" branch.
Squashed Merge
This is the last and probably less used of the three merging strategies: it is called like so since it squashes all of the commits of the source branch (in other words, it packs them all into a single commit), melting everything into the target branch.
Please note that it stops right after melting: it is up to you then to proceed with a new commit that permanently adds the squashed changes into the Repository Area. Then you can safely remove the squashed branch.
Let's see an example with two diverged branches, first let's checkout the master branch:
git checkout master
then checkout the new "f-super" branch from the master:
git checkout -b f-super
echo "1st change" > super.txt
git add super.txt; git commit -m "super - 1st commit"
echo "2nd change" >> super.txt
git add super.txt; git commit -m "super - 2nd commit"
then checkout the "master" feature again, modify the contents and commit:
git checkout master
echo "8th change" > amazing.txt
git add amazing.txt; git commit -m "master - 5th commit"
lastly, let's checkout the "f-super" branch again, modify the contents and commit:
git checkout f-super
echo "3rd change" >> super.txt
git add super.txt; git commit -m "super - 3rd commit"
let's checkout the master branch:
git checkout master
and have a look at the log again - this time, to limit the output, we set the boundaries focusing only on the commits between the "master" branch and the "f-super" branch:
git log --decorate --oneline --graph --boundary master...f-super
the output is as follows:
* 7ad24d8 (f-super) super - 3rd commit
* 53cac2a super - 2nd commit
* f6960ce super - 1st commit
| * e9ac190 (HEAD -> master) master - 5th commit
|/
o 0fd7834 Merge branch 'f-new'
as you see, "0fd7834", that is the ancestor of the "f-super" branch, is behind the "master", indeed:
git rev-list --left-right --count 0fd7834...master
returns the following output:
0 1
let's check the files currently extracted from the repository:
ls -1
the output is as follows:
README.md
amazing.txt
new.txt
nice.txt
since we are in the "master" branch, it is right that the "super.txt" file is not listed, since it has been committed (three times) into the "f-secure" branch.
We can now squash all the commits currently in the "f-super" branch into the "master" branch as follows:
git merge --squash f-super
the output is as follows:
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested
Let's see the contents of the Staging Area:
git status
the output is as follows:
On branch master
Changes to be committed:
(use "git restore --staged ..." to unstage)
new file: super.txt
so the "super.txt" file, coming from the "f-super" branch, has been added to the Staging Area. As you can easily argue, it has been added to the Working Area too:
ls -1
the output is as follows indeed:
README.md
amazing.txt
new.txt
nice.txt
super.txt
as for the contents of the "super.txt" file:
cat super.txt
the output is as follows:
1st change
2nd change
3rd change
so all of the three commits have been melted down into the "master" branch, ... but the changes are still uncommitted.
Let's commit them now:
git commit -m "squashed f-super branch into master branch"
this time the output is as follows:
[master 7244c80] squashed f-super branch into master branch
1 file changed, 3 insertions(+)
create mode 100644 super.txt
if now we try to delete the "f-super" branch:
git branch -d f-super
we get the following error:
error: The branch 'f-super' is not fully merged.
If you are sure you want to delete it, run 'git branch -D f-super'.
since we squashed all the commits as a single change into the "master" branch, Git points out that there are commits that have never been merged, and so asks if we are sure of what we are doing.
Since in this case it is expected, we can force the deletion of the branch using the capital "d" as suggested by the message:
git branch -D f-super
if we have a look to the log again:
git log --all --decorate --oneline --graph
the output is as follows:
* 7244c80 (HEAD -> master) squashed f-super branch into master branch
* e9ac190 master - 5th commit
* 0fd7834 Merge branch 'f-new'
|\
| * 37ee1e7 new - 3rd commit
| * e4d4279 new - 2nd commit
| * a68267a new - 1st commit
* | 91f5697 master - 4th commit
* | 639af9f master - 3rd commit
|/
* 0cac194 nice - 3rd commit
* f7804be nice - 2nd commit
* 78400b2 nice - 1st commit
* 91a331f master - 2nd commit
* b10d323 master - 1st commit
* 96e6c84 amazing - 3rd commit
* 471bf17 amazing - 2nd commit
* 9d46249 amazing - 1st commit
* 8add783 added the heading
* 86cb5d7 Initial commit
we see that there's only the "7244c80" commit that contains all the changes of the "f-super" branch.
Revert A Merge
As an example, let's see how to revert a merge of branches we made by mistake right after having done that merge.
Let's start having a look to the git log:
git log
the outcome is as follows:
commit 643621b6330bf5e4e45934a603edfa4b9a666786 (HEAD -> master, origin/master, origin/HEAD)
Merge: 1155e73 e474561
Author: Marco Carcano <marco@carcano.local>
Date: Thu May 11 18:14:16 2022 +0200
Merge branch 'f-foo' into 'master'
Foo feature: added a fancy feature
from the log we can guess that the "f-foo" branch (e474561) has been merged into the "master" (1155e73) branch.
We can revert the "master" branch as it were before the merge: first checkout the "master" branch:
git checkout master
then type:
git reset --hard 1155e73
Tagging
As we already saw, tags are names used to conveniently identify one and only one hash: their purpose is to provide a human-readable name to refer to a specific hash so as to ease checking out contents of a given point of the story. This means that tags have a repository-wide scope (they do not belong to any branch), and so their names must be unique across every branch.
Listing Tags
You can list the available tags by typing:
git tag
Creating A Tag
To tag the current commit simply type "git tag" followed by the name you want to assign to the tag and an optional descriptive message.
For example, to tag as "0.1.3" with the message "Release 0.1.3" type:
git tag 0.1.3 -m "Release 0.1.3"
Deleting A Tag
If instead you want to delete a tag, type "git tag -d" followed by the name you want to delete.
For example, to delete "0.1.3" tag type:
git tag -d 0.1.3
Checking Out A Tag
To use the tagged commit you must checkout its contents: so if you want to use tag "0.1.3", type:
git checkout tags/0.1.3
Hooks
GIT provides a set of hooks that can be exploited to intercept events and perform additional actions. You can find sample files beneath the ".git/hooks" directory.
A really handy hook is the "pre-commit" hook that can be used for example to run checks before your code is actually committed: you can exploit it for example to validate the syntax of the contents of the file before your code is actually committed.
For example when dealing with Ansible I like to create into my repositories a ".git/hooks/pre-commit" file with the following contents:
#!/bin/bash
exit_status=0
for file in $(git diff-index --name-only --cached HEAD --)
do
if [[ ${file} == *.yml ]] && ! ansible-lint ${file}
then
echo "Commit failed: ansible-lint cannot parse ${file}"
exit_status=1
fi
done
exit ${exit_status}
Of course this hook requires having already installed the ansible-lint: this is a utility that checks the compliance of the Ansible YAML files to several best practices.
The above lines of code basically make a list of modified files: if the name end by ".yml" it verifies its syntax parsing it with ansible-lint. The first time a file does not pass the validation, the hook exits printing an error message and returning "1", causing the commit to fail.
The straightforward benefit of such an hook is preventing you from committing not compliant or invalid code.
Let's try it as follows: create a trivial Ansible playbook with the following contents, and save it as facts.yml:
---
- name: An example playbook
hosts: localhost
tasks:
- ansible.builtin.debug:
var: ansible_fact
Now add the file to the Staging Area and try to commit it:
git add --all
git commit -m "precommit hook test"
the output is:
WARNING Overriding detected file kind 'yaml' with 'playbook' for given positional argument: facts.yml
WARNING Listing 1 violation(s) that are fatal
name: All tasks should be named. (name[missing])
facts.yml:5 Task/Handler: debug var=ansible_facts
You can skip specific rules or tags by adding them to your configuration file:
# .config/ansible-lint.yml
warn_list: # or 'skip_list' to silence them completely
- name[missing] # Rule for checking task and play names.
Finished with 1 failure(s), 0 warning(s) on 1 files.
Commit failed: ansible-lint cannot parse facts.yml
As you see the playbook is not in compliance with the best practices: to fix it, just add the "name" attribute to the task as suggested by yaml-lint.
It must now look like as follows:
---
- name: An example playbook
hosts: localhost
tasks:
- name: Print every fact of this host
ansible.builtin.debug:
var: ansible_fact
To retry, we must add it to the Staging Area again and then commit:
git add --all
git commit -m "precommit hook test"
the output is:
[master 1b1a7d6] precommit hook test
1 file changed, 7 insertions(+)
create mode 100644 facts.yml
Since ansible-lint succeeded, this time the commit worked.
Footnotes
Here ends this first post on using Git for version control: we saw thoroughly everything you may face in your daily work when operating version control with it. In the next post we'll explore in detail how to work with remote repositories. Stay tuned.
2 thoughts on “GIT Tutorial – A thorough Version Control with Git Howto”