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.

Typical workflow

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.

Git conventions and naming

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:

  • start with a capital,
  • the first word is a verb in the present tense, the imperative form,
  • no period at the end, and
  • a maximum of 50 characters.

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.

Set up 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.

Acquire the configuration files

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:

Set up the text editor

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.

Set up Bash

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.

Set up Git

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

Try out Git

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.

Initialize a Git 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

Use a server as a remote

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.

Collaboration

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.

Cloning

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.

Colby adding content

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]                                               
$ 

Parallel collaboration

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.

Working in different branches

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.

Merging branches

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]                                                 
$ 

Recap of Git concepts

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.