Git is a distributed version control system and is currently one of the most popular version control systems. A version control system is a system that keeps track of versions of files and is often used in software development. By means of the version control system, we keep a history of the various versions of the files and it allows us to move back to earlier versions. In addition, it allows people to collaborate on the same set of files and it makes clear which person made which changes.
Version control systems come in two flavors: centralized and distributed. With centralized version control systems, there is one server that the various collaborators use to register the changes. This centralized server keeps track of all the changes and the collaborators typically have a snapshot of the current state of the set of files they are working on. This is very effective for companies that want to protect their source code for example. However, for open source software development, this model is less ideal because the central server acts as a single point of failure: If the administrator of the server —typically a core developer— shuts down the server or quits, then the open source project is likely to die because the version history is gone and the other collaborators have no longer a server to collaborate on.
In contrast, distributed version control systems are systems where each collaborator has access to the complete history of the repository and if a key maintainer quits and shuts down the main server, any collaborator can immediately set up a new main entry point for collaboration.
The typical workflow in Git is that a person works on a set of files and makes changes to the files. When this person is satisfied with the changes, he or she would commit these changes. The act of committing records the changes in the version control system and this commit becomes a point of reference, a kind of checkpoint. This means that if the changes are incorrect, we can revert to an earlier commit, undoing the incorrect changes.
Git repositories typically have a main
branch. A
branch is a series of commits with a name and git repositories
can have multiple branches. A typical use-case is a developer
anticipating two different solutions for a problem. So, the developer
can create a branch solution-1
on top of branch
main
and try the first solution by making a couple of
commits in branch solution-1
. Then, the developer could
move back to the main
branch and start a branch
solution-2
there and make a couple of commits there.
Depending on what the best solution is, the developer can then either
merge branch solution-1
or solution-2
back into the main
branch. Merging is the act of
adding the commits of one branch to another.
This branching is also the main way to collaborate between persons.
Collaborators would have a version of the complete history of the Git
repository, a so called clone of the repository on their
computer. They would create a branch and work on that branch, making
several commits. When they are satisfied with their work, they would
push their branch to the main server, making their commits
visible to the other collaborators. One of the collaborators can then
decide to merge this branch into the main
branch. We will
try out these workflows below, but let’s first focus on important
conventions in Git.
In programming and perhaps in general, naming is very important. When recording the history of an open source project, it is very important that collaborators, the community can understand what this history is and what is being worked on.
At many points in the development of open source hardware, developers
are confronted with a decision to name something: the name of a
part in CAD design, the name of a
parameter that influences the design, the name of a
branch in a Git repository, the name of the
repository itself, the summary of a commit
message, etc. Naming is a very challenging subject because as a
developer, software or hardware, you have to think of how
general the thing is that you are naming and how
specific it is to the current context. For example,
when designing an aluminium profile, a good name may be
aluminium-profile
. However, is the design specific to the
material aluminium? If not, then it may be better to remove the
aluminium
part. However, then the name becomes
profile
and this is perhaps a name that is too general:
based on this name, developers may have no clue what kind of part this
is, whereas with the name aluminium-profile
they would
immediately understand what is meant. So, it may be better to name it
aluminium-profile
after all. In addition, this design may
be used multiple times and as such you may want to name a specific
instance left-profile
or right-profile
.
For recording the version history, it is also important to name various things well. Additionally, there are many conventions in place that help developers understand the version history. When contributing to a Git repository it is good practice to become familiar with the conventions used for the project and adopt them as well. Below we list some default conventions.
The names of Git repositories themselves typically contain lowercase
characters with dashes, sometimes referred to as kebab-case
or dash-case. The name should not be too long but also not too
short to loose specificality. For example this document is part of the
repository oss-for-osh-primer
. Names of branches in the Git
history often follow the same convention, dash-case and not too long but
specific.
Commit messages are very important to keep track of the version history and the general advise is to take care in specifying them. Some of the commit messages are suggested by Git itself and the following convention originates from that. A Git commit message consists of at least a summary and an optional description. To allow good processing with command line tools, the common convention for the summary is as follows:
A couple of examples:
Merge branch 'feature' into main
(often generated by
Git itself)Add a description of the project
Change the size of the 3D printer
For the description it is important to leave an empty line between the summary above and the description. Stick to a maximum of 70 characters per line and focus on explaining what you did and why. A great resource with examples is on Chris Beam’s blog. Before we put this in practice, let us set up a good environment for working with Git.
Powerful editors and IDEs typically have support for Git, which means that you can control Git repositories from these tools. However, it is important to know the command line tool because this allows you to do all operations on Git repositories and since many people use the command line tool, it may be easier to look up things such as issues online based on the command line tool.
To work comfortably with Git in the command line, we can improve our command line environment. We will set up an editor, Bash, and Git itself.
To acquire the necessary configuration file we are going to make use
of Git itself. Test whether Git is installed by executing
git
and if not, install it with one of these commands, for
Debian-based systems such as Ubuntu:
sudo apt install git
For Arch-based systems:
sudo pacman -S git
For MacOS install the package manage Homebrew and execute:
brew install git
Now we can clone the repository with configuration files:
git clone https://gitlab.fabcity.hamburg/software/config-files-for-git-workflow.git
This may look like this for user Jamie that is on his or her computer
computer
:
[jamie@computer ~]$ git clone https://gitlab.fabcity.hamburg/software/config-files-for-git-workflow.git Cloning into 'config-files-for-git-workflow'... remote: Enumerating objects: 22, done. remote: Counting objects: 100% (4/4), done. remote: Compressing objects: 100% (4/4), done. remote: Total 22 (delta 0), reused 0 (delta 0), pack-reused 18 Receiving objects: 100% (22/22), 9.15 KiB | 9.15 MiB/s, done. Resolving deltas: 100% (2/2), done. [jamie@computer ~]$
The README in this repository explains how to set up the configuration files but we explain it here as well:
Firstly, we are setting up nano
as a text editor to work
with Git. Execute the commands below to setup nano
. First
we enter the Git repository and copy the Nano resource file to the home
directory as a hidden file (with a dot in front of it). Don’t forget
auto completion (cd c<Tab>
,
cp c<Tab>n<Tab>
). In Unix systems, there is a
shorthand for the home directory, namely ~
, so typically
the home directory of Jamie would be /home/jamie
and
~
refers to that directory.
cd config-files-for-git-workflow
cp config-files/nanorc ~/.nanorc
This specific configuration file contains syntax highlighting rules
for working with Git. Each time nano
opens, it will look
for the .nanorc
file in the home directory, process the
resource file, check whether the file you opened matches one of the
syntax rules and apply them if this is the case.
We will modify the Bash prompt to give information about Git
repositories. It is best to use a dark background in the terminal for
this setup. We execute the commands below. Copy the Bash prompt to your
home directory as a hidden file
(cp c<Tab>b<Tab>
):
cp config-files/bash_prompt ~/.bash_prompt
It is then best to make a backup of your Bash resource file. Here you
can auto complete twice
(cp ~/.ba<Tab>r<Tab> ~/.ba<Tab>r<Tab><Backspace>_bak
):
cp ~/.bashrc ~/.bashrc_bak
Load the Bash prompt configuration in your Bash resource file
(Note the >>
is
very important (appending to a file); it should not be
>
(overwriting a file)):
echo "source $HOME/.bash_prompt" >> ~/.bashrc
Each time Bash starts, it will read the ~/.bashrc
file.
The last line of this file that we’ve just added will load our file
.bash_prompt
and set up the prompt. Reopen the terminal or
start Bash again:
bash
The output should be something similar to:
/home/jamie/config-files-for-git-workflow [main] $
We can see that the Bash prompt has changed. More specifically, it shows the directory you are in in the color green and it mentions which Git branch we are on if we are in a Git repository.
We will also set up a configuration file for Git
(cp co<Tab>g<Tab>
):
cp config-files/gitconfig ~/.gitconfig
Each time git
is executed, it will read the
configuration file and apply the settings defined there. Let us add a
bit more personal information to the configuration file by executing the
following commands below, replacing “Jamie Doe” with your name and the
email address with your email address. Note that we use quotes in the
first command because the program expects one argument after
user.name
. With the quotes “Jamie Doe” becomes one argument
whereas with two quotes it would become two arguments. We do not need
quotes for the email address because there are no spaces in email
addresses and as such it is interpreted as one argument. Note that you
can use auto complete here as well
(git con<Tab> --gl<Tab>us<Tab>n<Tab>
):
git config --global user.name "Jamie Doe"
git config --global user.email jamiedoe@server.org
This command changed the file ~/.gitconfig
and we can
verify this in the text editor nano
. Note the syntax
highlighting for this file and don’t forget auto completion.
nano ~/.gitconfig
The output should look similar to below. Remember that to close Nano,
we have to press ^X
which means Ctrl-x
.
GNU nano 7.2 /home/jamie/.gitconfig # SPDX-FileCopyrightText: 2021 Pieter Hijma <pieter@hiww.de> # # SPDX-License-Identifier: CC0-1.0 [user] name = Jamie Doe email = jamiedoe@server.org [init] defaultBranch = main [core] editor = nano [color] status = auto [color "status"] added = green bold changed = yellow bold [ Read 32 lines ] ^G Help ^O Write Out ^W Where Is ^K Cut ^T Execute ^C Location ^X Exit ^R Read File ^\ Replace ^U Paste ^J Justify ^/ Go To Line
We are now ready to experiment with Git. Let’s execute the following to go back to the home directory:
cd
We are finally all set, so let’s try Git! We will create a Git repository, create a remote repository and experiment with commits and branches. Let’s assume we have three parties involved: you, a collaborator that we are going to call col and a server for the remote repository.
Let’s create a directory that will become our Git repository and
let’s give it a name that reflects what this repository will be, namely
a test repository for Git. We could name it
test-repo-for-git
but every Git repository is, well, a
repo
, so let’s remove that part: test-for-git
.
We can make the name even shorter by removing for
and we
would still understand what this directory or repository would contain,
so we suggest: test-git
and let’s put this in the directory
you
because you are the one starting the repository:
mkdir -p you/test-git
cd you/test-git
We are now in the test-git
directory and we can make it
a Git repository by executing:
git init
/home/jamie/you/test-git $ git init Initialized empty Git repository in /home/jamie/you/test-git/.git/ /home/jamie/you/test-git [main] $
Notice that the Bash prompt reflects that we are in a git repository
on branch main
. Some older versions of Git make a branch
master
instead of main
. In general the
community is moving away from the word master
and replaces
it with main
. Let’s follow this convention and if the
branch is called master
, let’s change it to
main
after we have made the first commit.
We can now verify the status of the Git repository:
git status
/home/jamie/you/test-git [main] $ git status On branch main No commits yet nothing to commit (create/copy files and use "git add" to track) /home/jamie/you/test-git [main] $
This command will tell us that we have an empty Git repository and
that we can add files. Let’s create the same file we did earlier and now
that we are focused on naming things correctly, let’s name the file such
that it reflects the contents. Note that on the first line you have to
type the full name of the file, but on the second line you can use auto
completion (>> e<Tab>
).
echo Is anyone there? >> echo-and-narcissus.txt
echo Come here! >> echo-and-narcissus.txt
echo Come here! >> echo-and-narcissus.txt
echo This way, we must come together. >> echo-and-narcissus.txt
Now, if we do git status
, we can see that we have
untracked files in red, meaning that Git does not know about
these files:
/home/jamie/you/test-git [main] $ git status On branch main No commits yet Untracked files: (use "git add <file>..." to include in what will be committed) echo-and-narcissus.txt nothing added to commit but untracked files present (use "git add" to track) /home/jamie/you/test-git [main] $
To track these files, we have to add them to the Git repository (using auto completion again):
git add echo-and-narcissus.txt
If we redo git status
, we can see that the file is
tracked and marked as green:
/home/jamie/you/test-git [main] $ git status On branch main No commits yet Changes to be committed: (use "git rm --cached <file>..." to unstage) new file: echo-and-narcissus.txt /home/jamie/you/test-git [main] $
When we do a commit, this version of the file is recorded in the Git repository:
git commit
The nano
editor will open with a special file that will
contain the commit message that specifies that this is an
initial commit. We can now enter a summary on the first line and a
description on the third line and below. Let’s do that:
Initialize test-git.git
To test git, we added a file with an excerpt from the myth Echo
and Narcissus from Ovid's Metamorphoses. This long description
serves as an example of editor support to keep messages within 70
characters.
When typing the commit message, a white line means that the commit message is within 50 characters, so sufficient for a summary message. Yellow lines mean that the line is between 50 and 70 characters and red lines are too long to be part of the description. When specifying commit message, please try to stick to these conventions:
GNU nano 7.2 /home/jamie/you/test-git/.git/COMMIT_EDITMSG Modified Initialize test-git.git To test git, we added a file with an excerpt from the myth Echo and Narcissus from Ovid's Metamorphoses. This long description serves as an example of editor support to keep messages within 70 characters. # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # # On branch main # # Initial commit # # Changes to be committed: # new file: echo-and-narcissus.txt # ^G Help ^O Write Out ^W Where Is ^K Cut ^T Execute ^C Location ^X Exit ^R Read File ^\ Replace ^U Paste ^J Justify ^/ Go To Line
We can now save the special file COMMIT_EDITMSG
that Git
created for us and by saving this file we commit the changes. We can
save with Ctrl-x
, answering y
and confirming
with Enter
.
The Bash prompt now reflects with a white [main]
or
[master]
branch that there are no uncommitted files and
that the working tree is clean:
/home/jamie/you/test-git [main] $ git commit [main (root-commit) 58ea77e] Initialize test-git.git 1 file changed, 4 insertions(+) create mode 100644 echo-and-narcissus.txt /home/jamie/you/test-git [main] $
We can verify this with:
git status
Now that we’ve made a first commit, let’s change the name of the
branch to main
if that wasn’t the case. Note that you can
use auto completion on master
.
git branch -m master main
Now that we have our repository, we may want to share with others to
collaborate, so let’s make a new directory server
besides
you
and clone the repository on that server:
cd ..
mkdir ../server
git clone --bare test-git ../server/test-git.git
/home/jamie/you $ git clone --bare test-git ../server/test-git.git Cloning into bare repository '../server/test-git.git'... done. /home/jamie/you $
We’ve just cloned a bare repository into the
server
directory. A bare repository is a
repository where you don’t have access to the set of files you are
working on but it does contain the whole history of the Git repository.
It is often used on servers for pushing changes to this repository.
We can now add this “server” (it is not actually a server but for Git
it doesn’t matter) as a remote to our Git repository. We can
give this remote any name we like, but we will follow the convention to
call the remote origin. In other words, we will register the
remote origin to share our changes. So, let’s go to
you/test-git
:
cd test-git
And add the remote origin
with the location of the bare
repository:
git remote add origin ../../server/test-git.git
At this point, we have added the server as a remote but we haven’t made any contact with the server. We can visualize the state of the commits with the following command:
git graph
/home/jamie/you/test-git [main] $ git graph * (HEAD -> main) Initialize test-git.git /home/jamie/you/test-git [main] $
This shows that the commit with message “Initialize test-git.git” is
the HEAD
of the repository. The HEAD
points to
the commit you are on, so we are “on this commit”. Let’s now make
contact with the remote server and fetch the commits that are
on this server:
git fetch
/home/jamie/you/test-git [main] $ git fetch From ../../server/test-git * [new branch] main -> origin/main /home/jamie/you/test-git [main] $
We can see that there is a new branch origin/main
added.
Let’s now do the following again:
git graph
/home/jamie/you/test-git [main] $ git graph * (HEAD -> main, origin/main) Initialize test-git.git /home/jamie/you/test-git [main] $
We can see that the HEAD
hasn’t changed but it says that
origin/main
is at the same commit.
Often, we let Git know which local branch tracks
which remote branch. A local branch is a branch on
your repository, so in this case in the repository
you/test-git
and a remote branch is a branch on
the server. Tracking a branch means that the local branch
follows the remote branch. Git will then mention whether and how many
commits you are behind or in front of the tracked remote branch. The
following command registers the origin branch
origin/main
as the branch to be tracked by branch
main
. Note that you can use auto completion on
origin
and main
.
git branch -u origin/main main
/home/jamie/you/test-git [main] $ git branch -u origin/main main branch 'main' set up to track 'origin/main'. /home/jamie/you/test-git [main] $
Now that we’ve told Git which branch tracks which remote branch, we
can push to and pull from origin
:
git pull
Git tells us that we are already up-to-date. With the following, we can push our changes to the server:
/home/jamie/you/test-git [main] $ git pull Already up to date. /home/jamie/you/test-git [main] $
git push
/home/jamie/you/test-git [main] $ git push Everything up-to-date /home/jamie/you/test-git [main] $
Git tells us that everything is up-to-date, so nothing happened. Let’s now start collaborating with another user named Colby.
Suppose we have a way to share the directory server
with
our collaborator Colby. There are many ways to do this, but a simple way
would be to share this directory on a cloud service such as Dropbox or
Google Drive. Colby could then clone from this shared directory and
create an own working copy.
Let’s try that out. We first make a directory for Colby, reflecting that Colby has his or her own computer.
cd ../../
mkdir colby
cd colby
Now we clone the repository from the server:
git clone ../server/test-git.git
/home/jamie/colby $ git clone ../server/test-git.git/ Cloning into 'test-git'... done. /home/jamie/colby $
This automatically creates the Git repository test-git
and we can inspect it when moving to the directory:
cd test-git
git status
git graph
/home/jamie/colby/test-git [main] $ git status On branch main Your branch is up to date with 'origin/main'. nothing to commit, working tree clean /home/jamie/colby/test-git [main] $ git graph * (HEAD -> main, origin/main, origin/HEAD) Initialize test-git.git /home/jamie/colby/test-git [main] $
We can see that Colby has the complete history of the Git repository
with a local branch main
pointing to the remote branch
origin/main
. This means that Git automatically created a
remote origin
for us and branch main
is
tracking branch origin/main
. We can see the location of the
remote with the command:
git remote -v
/home/jamie/colby/test-git [main] $ git remote -v origin /home/jamie/colby/../server/test-git.git/ (fetch) origin /home/jamie/colby/../server/test-git.git/ (push) /home/jamie/colby/test-git [main] $
Let’s change the name and email address for Colby in this repository:
git config user.name "Colby Laborator"
git config user.email colby@laborator.com
Note that by leaving out the option --global
as we did
before, we configure the current Git repository instead of writing into
~/.gitconfig
that contains the global settings for this
computer.
Suppose you and Colby agree that you’ll continue working on Echo and Narcissus, while Colby will work on Achilles and Cycnus in a separate file. Let’s create a file for Colby with part of the Achilles story:
echo "Immediately he threw his spear against Achilles" >> achilles-and-cycnus.txt
If we request the status, we can see that there is an untracked file that we could add:
git status
/home/jamie/colby/test-git [main] $ git status On branch main Your branch is up to date with 'origin/main'. Untracked files: (use "git add <file>..." to include in what will be committed) achilles-and-cycnus.txt nothing added to commit but untracked files present (use "git add" to track) /home/jamie/colby/test-git [main] $
The status message also tells us that we are on branch
main
that is up-to-date with origin/main
. If
we add and commit then we will have committed in branch
main
. We can then push our changes to the server, to
origin
, so let’s do that:
git add .
With git status
we can see that there are changes to be
committed:
/home/jamie/colby/test-git [main] $ git status On branch main Your branch is up to date with 'origin/main'. Changes to be committed: (use "git restore --staged <file>..." to unstage) new file: achilles-and-cycnus.txt /home/jamie/colby/test-git [main] $
Let’s commit with the following message:
Add text for Achilles and Cycnus
The story is in a separate file.
git commit
In the bash prompt we can see that branch main
has
turned yellow meaning that there are unpushed changes. With
git status
we can see that our branch main
is
ahead of origin/main
by 1 commit:
/home/jamie/colby/test-git [main] $ git status On branch main Your branch is ahead of 'origin/main' by 1 commit. (use "git push" to publish your local commits) nothing to commit, working tree clean /home/jamie/colby/test-git [main] $
We can push our changes to the server origin
:
git push
/home/jamie/colby/test-git [main] $ git push Enumerating objects: 4, done. Counting objects: 100% (4/4), done. Delta compression using up to 8 threads Compressing objects: 100% (2/2), done. Writing objects: 100% (3/3), 383 bytes | 383.00 KiB/s, done. Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 To /home/jamie/colby/../server/test-git.git/ 58ea77e..1f4f88c main -> main /home/jamie/colby/test-git [main] $
Let’s now focus our attention to you again:
cd ../../you/test-git
If we perform git status
we don’t see the changes of
Col. We can include the changes from Colby by performing
git pull
but let’s not do that yet!
Suppose you are also working in parallel to Colby and added some new text to your file:
echo "Hands off!" >> echo-and-narcissus.txt
Let’s add and commit these changes with the following commit message:
Add more text to Echo and Narcissus
The commands:
git add .
git commit
This should look similar to:
/home/jamie/you/test-git [main] $ echo "Hands off!" >> echo-and-narcissus.txt /home/jamie/you/test-git [main] $ git add . /home/jamie/you/test-git [main] $ git commit [main 486a546] Add more text to Echo and Narcissus 1 file changed, 1 insertion(+) /home/jamie/you/test-git [main] $
Let’s now push our changes to the server origin
:
git push
/home/jamie/you/test-git [main] $ git push To ../../server/test-git.git/ ! [rejected] main -> main (fetch first) error: failed to push some refs to '../../server/test-git.git/' hint: Updates were rejected because the remote contains work that you do hint: not have locally. This is usually caused by another repository pushing hint: to the same ref. You may want to first integrate the remote changes hint: (e.g., 'git pull ...') before pushing again. hint: See the 'Note about fast-forwards' in 'git push --help' for details. ERROR: 1 /home/jamie/you/test-git [main] $
We are unable to do so because origin/main
has advanced
to a new commit because Colby made a commit on his or her local branch
main
and pushed that commit to
origin/main
.
If we perform git graph
in our repository, we can see
that origin/main
points to the first commit. Unfortunately
this does not reflect reality because origin
has advanced
branch origin/main
to a new commit.
/home/jamie/you/test-git [main] $ git graph * (HEAD -> main) Add more text to Echo and Narcissus * (origin/main) Initialize test-git.git /home/jamie/you/test-git [main] $
Pulling in changes from a remote is good practice before you push, so let’s try to do that now:
git pull
/home/jamie/you/test-git [main] $ git pull remote: Enumerating objects: 4, done. remote: Counting objects: 100% (4/4), done. remote: Compressing objects: 100% (2/2), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (3/3), 363 bytes | 363.00 KiB/s, done. From ../../server/test-git 58ea77e..1f4f88c main -> origin/main hint: You have divergent branches and need to specify how to reconcile them. hint: You can do so by running one of the following commands sometime before hint: your next pull: hint: hint: git config pull.rebase false # merge hint: git config pull.rebase true # rebase hint: git config pull.ff only # fast-forward only hint: hint: You can replace "git config" with "git config --global" to set a default hint: preference for all repositories. You can also pass --rebase, --no-rebase, hint: or --ff-only on the command line to override the configured default per hint: invocation. fatal: Need to specify how to reconcile divergent branches. ERROR: 128 /home/jamie/you/test-git [main] $
Git tells us that it is not possible to fast-forward, because branch
main
and origin/main
have diverged. This means
that we cannot move branch main
to the same commit as
origin/main
. The culprit is that we already have a
different commit on main
that is now conflicting with
origin/main
. Let’s inspect this further:
git graph
/home/jamie/you/test-git [main] $ git graph * (HEAD -> main) Add more text to Echo and Narcissus | * (origin/main) Add text for Achilles and Cycnus |/ * Initialize test-git.git /home/jamie/you/test-git [main] $
We can see that the branches main
and
origin/main
have diverged.
This phenomenon of divergence happens a lot when people collaborate and work in parallel. Dealing with this form of parallel collaboration is exactly why versioning systems were made. Please note that parallel collaboration is also difficult in the physical world. Git may seem daunting, but all these mechanisms are developed to make parallel collaboration manageable.
The problem we have encountered here stems from the fact that
multiple people work on the same branch, in this case the branch
main
. Generally, we try to perform parallel work in
different branches and at proper moments in time, we merge these
branches main
, typically by a core developer. These
branches have well-defined names that indicate what work is being done
and are often called topic branches. After completion of the
work, the topic branch is merged into the main
branch with
the goal to make the changes known to the other collaborators.
So, let’s try to solve the above problem and let’s inspect the graph again:
git graph
/home/jamie/you/test-git [main] $ git graph * (HEAD -> main) Add more text to Echo and Narcissus | * (origin/main) Add text for Achilles and Cycnus |/ * Initialize test-git.git /home/jamie/you/test-git [main] $
We can see that our HEAD
is on main
and
origin/main
diverged from the commit before
HEAD
. Let’s create a topic branch for our work at the same
point in history where the current branch main
is:
git branch echo-and-narcissus main
When we inspect with git graph
, we can see that we have
a new branch echo-and-narcissus
that is at the same commit
as main
.
/home/jamie/you/test-git [main] $ git graph * (HEAD -> main, echo-and-narcissus) Add more text to Echo and Narcissus | * (origin/main) Add text for Achilles and Cycnus |/ * Initialize test-git.git /home/jamie/you/test-git [main] $
Let’s now do something more advanced: With git graph
we
can see that we are on branch main
, or HEAD
points to main
. Let’s reset HEAD
and
main
to the previous commit, so to the commit where there
was no divergence. This is an advanced operation that can result in
losing your commits if not done correctly. Since we associated branch
echo-and-narcissus
with the current commit, we will not
lose commits. However, if we would not have done that, we would have
lost this commit, because no branch is pointing to it. Therefore, with
the echo-and-narcissus
pointing to the same commit we are
on, we can safely reset the head to the previous commit. The following
commit resets the branch main
we are on (the
HEAD
) to the commit HEAD
minus (the
~
) 1:
git reset HEAD~1
git graph
/home/jamie/you/test-git [main] $ git reset HEAD~1 Unstaged changes after reset: M echo-and-narcissus.txt /home/jamie/you/test-git [main] $ git graph * (echo-and-narcissus) Add more text to Echo and Narcissus | * (origin/main) Add text for Achilles and Cycnus |/ * (HEAD -> main) Initialize test-git.git /home/jamie/you/test-git [main] $
We can see that we have the same commits but that HEAD
now points to the first commit and that our last commit is still
referenced by branch echo-and-narcissus
. With
git status
we can see that there changes to
echo-and-narcissus.txt
:
/home/jamie/you/test-git [main] $ git status On branch main Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded. (use "git pull" to update your local branch) Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: echo-and-narcissus.txt no changes added to commit (use "git add" and/or "git commit -a") /home/jamie/you/test-git [main] $
However, on inspection, it appears to be exactly the changes that we
already committed in branch echo-and-narcissus
. Let’s
confirm this by showing the difference between the current files and
branch echo-and-narcissus
:
git diff echo-and-narcissus
/home/jamie/you/test-git [main] $ git diff echo-and-narcissus /home/jamie/you/test-git [main] $
We can see that there is no output, meaning that there is no
difference. When resetting, Git left the file as it was, which means
that the file is different with respect to the current
HEAD
. We can verify this by checking the difference with
the current branch:
git diff
/home/jamie/you/test-git [main] $ git diff diff --git a/echo-and-narcissus.txt b/echo-and-narcissus.txt index 31f51ce..22099d7 100644 --- a/echo-and-narcissus.txt +++ b/echo-and-narcissus.txt @@ -2,3 +2,4 @@ Is anyone there? Come here! Come here! This way, we must come together. +Hands off! /home/jamie/you/test-git [main] $
We can see that the file echo-and-narcissus.txt
has
changed with one line. Since this change is already captured by the
commit that branch echo-and-narcissus
references, we can
safely restore this file, throwing away these changes:
git restore echo-and-narcissus.txt
Note that the [main]
has changed colors from red
(meaning some files were changed) to white (meaning that there are no
changes):
/home/jamie/you/test-git [main] $ git restore echo-and-narcissus.txt /home/jamie/you/test-git [main] $
When we do git status
we can see that branch
main
is 1 commit behind origin/main
and can be
fast-forwarded.
/home/jamie/you/test-git [main] $ git status On branch main Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded. (use "git pull" to update your local branch) nothing to commit, working tree clean /home/jamie/you/test-git [main] $
Fast-forwarding means that branch main
will simply be
set to the commit of origin/main
. So, let’s incorporate the
changes from remote origin
by doing:
git pull
/home/jamie/you/test-git [main] $ git pull Updating 58ea77e..1f4f88c Fast-forward achilles-and-cycnus.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 achilles-and-cycnus.txt /home/jamie/you/test-git [main] $
This time, it is possible to fast-forward main
to the
commit origin/main
is pointing, because from the point of
view of main
there is no divergence with respect to
origin/main
. With git graph
we see that
main
and origin/main
are pointing to the same
commit.
/home/jamie/you/test-git [main] $ git graph * (echo-and-narcissus) Add more text to Echo and Narcissus | * (HEAD -> main, origin/main) Add text for Achilles and Cycnus |/ * Initialize test-git.git /home/jamie/you/test-git [main] $
As you can see, these operations are quite complicated. We have to
perform these operations because parallel work was done in the same
branch leading to these kind of conflicts. Since you and Colby work in
parallel on different things, it is wise to tell Colby to create a
dedicated topic branch and push that branch to the remote. It would also
be wise to agree who merges the branches into the main
branch.
Let’s show proper parallel workflow and let’s switch back to Colby for a moment:
cd ../../colby/test-git
Colby was requested to create a dedicated topic branch and work on that. Let’s first get the most recent changes:
git pull
/home/jamie/colby/test-git [main] $ git pull Already up to date. /home/jamie/colby/test-git [main] $
We are already up-to-date, so we create a new branch, branching from
the branch we are on, namely main
. We have to find a proper
name that reflects the work we are doing so let’s call it
achilles-and-cycnus
.
git checkout -b achilles-and-cycnus
/home/jamie/colby/test-git [main] $ git checkout -b achilles-and-cycnus Switched to a new branch 'achilles-and-cycnus' /home/jamie/colby/test-git [achilles-and-cycnus] $
This created a new branch that we immediately switched to. With
git graph
we can see that HEAD
points to
achilles-and-cycnus
and that this is the same commit that
main
and origin/main
are pointing to.
/home/jamie/colby/test-git [achilles-and-cycnus] $ git graph * (HEAD -> achilles-and-cycnus, origin/main, origin/HEAD, main) Add text for Achilles and Cycnus * Initialize test-git.git /home/jamie/colby/test-git [achilles-and-cycnus] $
When we commit on this branch, achilles-and-cycnus
will
advance but main
will keep on pointing to the current
commit.
Let’s add some content:
echo "destined to pierce the curving shield through brass," >> achilles-and-cycnus.txt
Let’s add the file and commit with the following commit message:
Add text
git add .
git commit
/home/jamie/colby/test-git [achilles-and-cycnus] $ git commit [achilles-and-cycnus f847c89] Add text 1 file changed, 1 insertion(+) /home/jamie/colby/test-git [achilles-and-cycnus] $
Note that the commit message is shorter than the previous commit that
Colby performed. The previous message was
Add text for Achilles and Cycnus
. We performed that commit
in branch main
where it is good to show your collaborators
that you add text in the context of Achilles and Cycnus. However, since
we are now on branch achilles-and-cycnus
, this context is
already clear from this topic branch. As such we don’t need to provide
this extra context and can do with a shorter commit message. Let’s
inspect the commits with git graph
where we see that
achilles-and-cycnus
advanced from main
.
Let’s make our collaborators aware of our change and push this branch
to the remote. Since the remote is not aware of this branch (it does not
have origin/achilles-and-cycnus
), we have to explicitly
list the remote and the branch we are pushing:
git push origin achilles-and-cycnus
/home/jamie/colby/test-git [achilles-and-cycnus] $ git push origin achilles-and-cycnus Enumerating objects: 5, done. Counting objects: 100% (5/5), done. Delta compression using up to 8 threads Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 374 bytes | 374.00 KiB/s, done. Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 To /home/jamie/colby/../server/test-git.git/ * [new branch] achilles-and-cycnus -> achilles-and-cycnus /home/jamie/colby/test-git [achilles-and-cycnus] $
With git graph
we can see that there is now a branch
origin/achilles-and-cycnus
.
/home/jamie/colby/test-git [achilles-and-cycnus] $ git graph * (HEAD -> achilles-and-cycnus, origin/achilles-and-cycnus) Add text * (origin/main, origin/HEAD, main) Add text for Achilles and Cycnus * Initialize test-git.git /home/jamie/colby/test-git [achilles-and-cycnus] $
This does not necessarily mean that local branch
achilles-and-cycnus
tracks
origin/achilles-and-cycnus
, so let’s make sure it will:
git branch -u origin/achilles-and-cycnus
home/jamie/colby/test-git [achilles-and-cycnus] $ git branch -u origin/achilles-and-cycnus branch 'achilles-and-cycnus' set up to track 'origin/achilles-and-cycnus'. /home/jamie/colby/test-git [achilles-and-cycnus] $
We get a message that the current branch
achilles-and-cycnus
tracks the remote branch.
Let’s move back to you and let’s assume that you have completed the
work on Echo and Narcissus. Colby and you agreed that you would perform
the merges into main
, so you decide to merge branch
echo-and-narcissus
into main
. Let’s first
change directories to you:
cd ../../you/test-git
First, let’s obtain the new changes from the remote, because it may
be the case that origin/main
advanced without you knowing.
We are on branch main
that tracks the remote branch
origin/main
, so to obtain the changes we can perform:
git pull
/home/jamie/you/test-git [main] $ git pull remote: Enumerating objects: 5, done. remote: Counting objects: 100% (5/5), done. remote: Compressing objects: 100% (3/3), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (3/3), 354 bytes | 354.00 KiB/s, done. From ../../server/test-git * [new branch] achilles-and-cycnus -> origin/achilles-and-cycnus Already up to date. /home/jamie/you/test-git [main] $
Git tells us that we are already up-to-date, that is, branch
main
is up-to-date, but we can also see that a new branch
origin/achilles-and-cycnus
has been fetched. With
git graph
we can see that there is a new commit following
up the commit in main
:
/home/jamie/you/test-git [main] $ git graph * (origin/achilles-and-cycnus) Add text * (HEAD -> main, origin/main) Add text for Achilles and Cycnus | * (echo-and-narcissus) Add more text to Echo and Narcissus |/ * Initialize test-git.git /home/jamie/you/test-git [main] $
Since this is work that is independent of our work, we ignore it. We
just continue with our previous goal, namely merging
echo-and-narcissus
into main
.
To accomplish this we make sure that we are on the branch that we
want to merge into. This is main
and we are already on
main
, so that is good.
Then we execute the command below indicating the branch that we want to merge into the current branch.
git merge echo-and-narcissus
Nano will automatically open with a commit message and we will explain the reason, so the commit message becomes:
Merge branch 'echo-and-narcissus'
The work on Echo and Narcissus has completed.
GNU nano 7.2 /home/jamie/you/test-git/.git/MERGE_MSG Modified Merge branch 'echo-and-narcissus' The work on Echo and Narcissus has completed. # Please enter a commit message to explain why this merge is necessary, # especially if it merges an updated upstream into a topic branch. # # Lines starting with '#' will be ignored, and an empty message aborts # the commit. ^G Help ^O Write Out ^W Where Is ^K Cut ^T Execute ^C Location ^X Exit ^R Read File ^\ Replace ^U Paste ^J Justify ^/ Go To Line
We save the file and the merge has happened:
/home/jamie/you/test-git [main] $ git merge echo-and-narcissus Merge made by the 'ort' strategy. echo-and-narcissus.txt | 1 + 1 file changed, 1 insertion(+) /home/jamie/you/test-git [main] $
We can verify with git graph
and we see that there is a
commit originating from the original main
(where
origin/main
is now pointing to). This commit has two
so-called parents, the original main
and
echo-and-narcissus
. Our merge commit brought these
two commits together.
home/jamie/you/test-git [main] $ git graph * (HEAD -> main) Merge branch 'echo-and-narcissus' |\ | * (echo-and-narcissus) Add more text to Echo and Narcissus | | * (origin/achilles-and-cycnus) Add text | |/ |/| * | (origin/main) Add text for Achilles and Cycnus |/ * Initialize test-git.git /home/jamie/you/test-git [main] $
To publish our work to the other collaborators, we can push our
changes to remote origin
:
git push
/home/jamie/you/test-git [main] $ git push Enumerating objects: 9, done. Counting objects: 100% (8/8), done. Delta compression using up to 8 threads Compressing objects: 100% (5/5), done. Writing objects: 100% (5/5), 599 bytes | 599.00 KiB/s, done. Total 5 (delta 2), reused 0 (delta 0), pack-reused 0 To ../../server/test-git.git/ 1f4f88c..9ddbc65 main -> main /home/jamie/you/test-git [main] $
We can verify with git graph
that main
and
origin/main
point to the same merge commit. We can also see
that branch origin/achilles-and-cycnus
remains untouched by
our actions.
/home/jamie/you/test-git [main] $ git graph * (HEAD -> main, origin/main) Merge branch 'echo-and-narcissus' |\ | * (echo-and-narcissus) Add more text to Echo and Narcissus | | * (origin/achilles-and-cycnus) Add text | |/ |/| * | Add text for Achilles and Cycnus |/ * Initialize test-git.git /home/jamie/you/test-git [main] $
Now that we’ve seen a typical workflow, it is a good idea to recap some of the concepts of Git. Firstly, why are we using version control? Version control was invented to allow persons to collaborate and work in parallel on projects. Inevitably, working in parallel leads to conflicts and with proper version control we can resolve those conflicts, for example by working in separate branches. Version control also records the history that allows collaborators to understand the decisions that were made in the project. Naming commit messages, branches, repositories in a proper way, allows collaborators to understand the development and start contributing.
Because of these ideas, version control is very important for open source projects be it software or hardware. It allows people to contribute and become part of a community that embraces an open source project. Because Git is a distributed version control system, it is particularly useful for open source projects because all contributors have the complete history and when the core developers quit, any contributor can revive the project.
So, now that we understand the why, what exactly is a Git repository? A Git repository is essentially a special directory in which versions of the files in that directory are maintained. The files under version control are called the tracked files and on each commit a snapshot of all the tracked files is stored.
The most important and a basic concept of Git is the commit. A commit captures the current state of the tracked files and associates a unique number to the commit, the commit hash. It also captures the author of the commit and the parent commits.
If we do git log
in you/test-git
, we can
see that each commit has a long hexadecimal number associated with it
and the top commit explicitly references the two parents of this commit
with an abbreviation of the two commits, in my case 1f4f88c
and 486a546
.
commit 9ddbc65bc57153d5193c8ae55d6a5865fbb72ee5 (HEAD -> main, origin/main) Merge: 1f4f88c 486a546 Author: Jamie Doe <jamiedoe@server.org> Date: Mon Jan 30 15:26:13 2023 +0100 Merge branch 'echo-and-narcissus' The work on Echo and Narcissus has completed. commit 486a54634fdbcf2db9308f90438db62d6cdd5ffb (echo-and-narcissus) Author: Jamie Doe <jamiedoe@server.org> Date: Mon Jan 30 14:41:55 2023 +0100 Add more text to Echo and Narcissus commit 1f4f88c293b472bb874d7b30709ce98769fc8955 Author: Colby Laborator <colby@laborator> Date: Mon Jan 30 14:39:30 2023 +0100 Add text for Achilles and Cycnus The story is in a separate file. :
So, each commit except the initial commit is associated with one or more parent commits, that is, each commit references (a) previous commit(s) and as such the commits form a network of commits. In computer science we call such networks more generally a graph where the nodes are the commits and the edges between the nodes are formed by the parent-child relationship. Compare this to the graph of cities where each city is a node and the edges between the cities are the roads.
So, having this graph of commits – the basic concept of a Git repository – we also have branches that are merely labels, or references, or refs to a specific commit.
We can verify this with the following command:
/home/jamie/you/test-git [main] $ cat .git/refs/heads/main 9ddbc65bc57153d5193c8ae55d6a5865fbb72ee5 /home/jamie/you/test-git [main] $
cat .git/refs/heads/main
This file contains the hash of the commit branch
main
is pointing to. Note that in the .git
directory, all the metadata of the Git repository is stored.
So, now we understand the command git graph
better: It
shows you the graph of the Git repository, the graph of commits. Each
commit in this command is not presented by the commit hash, but by the
commit summary. The git graph
command also shows the
branches and to which commits they point.
Other tools to show the Git graph are gitk
that is most
likely already installed on your computer. An alternative is
gitg
. With gitk
we can browse the graph and we
can also see the changes that have been made in diffs.
These diffs can show you exactly what kind of changes have been made in
each commit. Since our test repository used text files, Git uses the
standard diff
tool for text to show the differences. This
highlights the power of using text files in Git repositories: It is easy
to see which changes have been made in each commit. For binary files, it
is not possible to use the diff
tool and although it is
possible to instruct Git to use a specialized diff
tool for
binary files, each type of binary file would require its own
diff
tool. Additionally, using specialized diff tools would
not be automatically incorporated into gitk
.