As a software/machine learning engineer, I have been using git for quite a while. In fact, I cannot even recall the first time I started using it at all. And as such I fell into the trap of thinking that every software engineer knows how to use git. That was until recently I started working on a side project with two of my fellows. It turned out that there are people in the field who have no understanding whatsoever about what git is and how to work with it. Even more embarrassing, the person I am talking about was actually using git at his main job but was completely unaware of what it is and how does it work! He was just shown several buttons in an IDE and was pressing them and hoping for the best. I volunteered to help him out and came up with a little presentation, which he found very useful. Today I want to share slightly shortened and polished version of this presentation with you. I hope that it can help at least some of you out there to stop being afraid of and start using git.

Please note that this article is aimed at people who are new to git. If you are already using it, you probably won’t gain much from it. Another thing to note is that as git is quite popular this days, there are many GUI tools to help you out when working with it. Most of the mainstream IDEs and text editors aimed at programmers have at least some kind of integration with git. Despite that I strongly recommend you start of using only the command line git tool and then, after gaining some intuition in what you are doing, you may incorporate some of the said tools if you find them useful. For that reason I will present all this article in pure command line using code blocks to show you the commands and their outputs.

For those of you who don’t feel confident enough to start working with command line: first of all, don’t - command line may seem bizarre when you start using it after using only GUI apps, but you should remember that it wasn’t created to overwhelm you, it simply takes some time to accustom to, and secondly, despite the fact that I would present all the commands using CLI, you may try to follow my actions using your GUI tool via searching for buttons implementing same commands.

  1. TOC {:toc}

What is git?

So what is git exactly and why would you want to use it? Git is a version control system (often referred to as VCS). This means that you can add something to it and git will keep track of its different versions for you as you change it. You can keep track of versions of pretty much anything with git, however, it works best with text files and source code in particular.

Why would you want to use a VCS? Lets imagine the following situation: you are managing some application and your team has just rolled out a new version of it. After a brief period of time, your users start using the new version and find a severe bug in it, which prevents them from using your app. Tackling this bug may take a while and in the meantime you might want to give your users an option to downgrade your app to a previous version so that they still can use it. How would you turn back the time and find the previous version of your source code (e.g. to recompile it)? With git it is as easy as to write one command!

Of course, this example is not very realistic. Surely, if you were managing an app, you could simply keep some of the previous versions’ binaries just in case of such disaster. However it gives you the general idea. And there is much more to VCS (and git in particular) than simply rolling back to previous versions the most important thing being the fact that git allows teams of engineers to work with source code collaboratively. This days applications tend to grow to such extent that one or two developers is just not enough to support and extend them. And when we are talking about a team (or even teams) of engineers, synchronizing their work can be a serious problem. Git simplifies this process to such extent that it actually allowed for the existence of open source projects where mere hundreds of absolutely unrelated people from all over the world contribute their work and with little effort it can be incorporated in the project source code without creating absolute mess out of it.

Basic git operations

When working with git we often say that we are working with a git repository. A repository is simply a directory, the state of which git keeps track of. There are two ways you can get hands on a repository: you can run git init in a directory, which will init a repository inside of it, or you can git clone existing repository from a remote storage. Let us focus on the former and discuss the latter in the following part of this article.

Let us create an empty directory somewhere on your system and open a terminal in it. If you run git init in it, you will see something like this:

> git init
Initialized empty Git repository in /path/to/your/example_repo/.git/

Quick note. If you are seeing message like this:

hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint: 	git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: 	git branch -m <name>

Simply run following commands:

> git config --global init.defaultBranch main
> git branch -m main

Don’t worry if you don’t understand the meaning of this commands. It should become clearer once you’ve read the part where I tell you about the branching.


If you were to check current contents of your previously empty directory now with ls -a, you would find that there is indeed a hidden subdirectory .git. This subdirectory (along with its contents) is what makes your directory a git repository. You might as well forget the git init command as you most likely will never run it again, but I want you to remember the following: each and every git repository starts from an empty repository. Even if you were to run the init command in a directory, populated with files, it will initialize empty repository and you would still need to add those files to it in order for git to start tracking their state.

Let’s add a few files to our new repository. However, before that I want to show you another command: git status. Run it and you would see something like this:

> git status
On branch main

No commits yet

nothing to commit (create/copy files and use "git add" to track)

This command prints out the status of your repository and it is arguably the most useful git command. Sure it does not allow you to actually change the repository state, however you still would probably run it a lot more often than any other. You changed some files and want to see all the changes before adding them into your repository? Run git status! You ran some commands and now you are not sure what is going on with your repository? Run git status! You are not even sure, that you are in a git repository? Run git status! It is extremely useful and I use it a lot and so should you.

Now let’s get back to adding some files to our repository:

> touch some_file.txt
> touch another_file.txt
> git status
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        another_file.txt
        some_file.txt

nothing added to commit but untracked files present (use "git add" to track)

The git status output starts to get more useful. It is telling you, that it noticed newly created files, however they are still untracked. This bit is very important: even though these new files are inside of the repository (in the way that they are inside of a directory, that is a git repository) git does not track your files until you explicitly tell it to. How do you do that? Git status actually tells you what to do: you need to use git add <file>. Let us do just that:

> git add some_file.txt
> git add another_file.txt
> git status
On branch main

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
        new file:   another_file.txt
        new file:   some_file.txt

Tip: two adds can be merged into one command: git add some_file.txt another_file.txt.

Now git actually tracks your files and if you attempt to modify them it will notice:

> echo "lorem ipsum" > some_file.txt
> git status
On branch main

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
        new file:   another_file.txt
        new file:   some_file.txt

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:   some_file.txt

Now our some_file.txt appears both in changes to be committed and in unstaged changes. You might think: we already added this file to our repository, why is git still complaining about unstaged changes in it? That is because with git you need to explicitly add all the changes to your repository. That is why right now git is aware of two states of some_file.txt: empty file, which we added previously, and the current state which is file with “lorem ipsum” in it. You might wonder: what is the deal with all those adds? Well with new files it is easy to understand: you might have some files in your working repository, which you might not want to keep track of such as building artifacts or .pyc files if we are talking about python project. However, what about changes in files that are already added to the repository? For this question there is no definitive answer and actually there is another VCS called Subversion or svn (it was very popular back in the day, however now it is loosing its popularity as many projects ditch it in favor of git) which actually requires you to only add new files, changes in already added files are always being tracked.

Also note how git status tries to tell you what you can do in particular situation with files. It shows you command to unstage staged files and commands to add or discard changes in some_file.txt. This is why git status is such a powerful tool: it often can save you a lot time and effort if you will pay attention to its output.

Let us add the changes in some_file.txt into our repository:

> git add some_file.txt
> git status
On branch main

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
        new file:   another_file.txt
        new file:   some_file.txt

Note that it now once again lists both our files in “changes to be committed” and also there is “no commits yet” message. This begs the question: what is a commit? Commit is a bunch of changes that you want to save in your repository’s history. Right now we have an empty repository. We added some files and git started to keep track of changes in those added file. But thats about all we have. Being able to have two versions of each file is not much of a version control, is it? That is because we still have not modified our repository history! And as I said earlier, we can do it with commit operation or git commit command. Lets do it. After you input git commit and hit enter you would most likely see the following message:

> git commit
Author identity unknown

*** Please tell me who you are.

Run

  git config --global user.email "you@example.com"
  git config --global user.name "Your Name"

to set your account's default identity.
Omit --global to set the identity only in this repository.

The problem here is that each commit must have an author. Without diving deep into the topic, I suggest you do exactly what its telling you to: run those commands fixing your name and email address. After that run git commit again and git will open a text editor with the following contents:


# 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:   another_file.txt
#	new file:   some_file.txt
#

This editor is here for you to enter the commit message. Besides author each commit must have a message. And it is very important that you think carefully about your commit messages, because they are what you and your colleagues will see in git history. A good commit message briefly describes what your commit does. For example, for our first commit I came up with the following message: add some files. So I typed it in the editor and hit save and quit. After that a commit should be made and your command prompt should look like this:

> git commit
[main (root-commit) 23efd47] add some files
 2 files changed, 1 insertion(+)
 create mode 100644 another_file.txt
 create mode 100644 some_file.txt

We see a short description of your commit including its message and a summary of the changes, i.e. addition of two files some_file.txt and another_file.txt. Let us run a git status once more:

> git status
On branch main
nothing to commit, working tree clean

Now it is much more concise as the git sees that we have already made some commits and our repository is clean (i.e. its state matches the one it should have considering history). However if we were to create yet another file or change one of the existing ones, git status will once again fill its output with useful tips and command suggestions:

> touch yet_another_file.txt
> echo "lorem ipsum" > another_file.txt
> git status
On branch main
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:   another_file.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        yet_another_file.txt

no changes added to commit (use "git add" and/or "git commit -a")

Let us add these changes into our repository and make another commit with commit message “add yet another file and modify another_file.txt”.

Tip: you can skip the text editor if you run git commit with -m option and provide it with your commit message as follows:

> git add another_file.txt yet_another_file.txt
> git commit -m "add yet another file and modify another_file.txt"
[main 37267c6] add yet another file and modify another_file.txt
 2 files changed, 1 insertion(+)
 create mode 100644 yet_another_file.txt

Now you are pretty much equipped to start working with local git repositories. One last thing you might want to consider is actually viewing your history. For that you have git log command, which will open your commit history with less utility. In our example repository we made two commits and so you should see something like this:

commit 37267c6c89a682672aad11d148997764e1fe7aca (HEAD -> main)
Author: Shiyanov Vadim <vadimsh853@gmail.com>
Date:   Fri Aug 6 21:11:59 2021 +0300

    add yet another file and modify another_file.txt

commit 23efd47319ef3c43b72587b422e747f049cb5f98
Author: Shiyanov Vadim <vadimsh853@gmail.com>
Date:   Fri Aug 6 20:39:40 2021 +0300

    add some files

To stop viewing the history, simply press q and you should come right back to your command line prompt.

Each commit description starts with line “commit” and a long hexadecimal number. That number is id of commit and it can be very useful. For example if you want to view changes introduced by that commit simple type git show <commit id>. Or if you want to see all the changes since some commit type git diff <commit id>.

Working with remote repository

Remember as I was telling you that the most important thing about git is the way it allows you to synchronize work of many people? How does it do that? Git does it through remote repositories. You might have heard about such web sites as GitHub, Bitbucket and GitLab (or others). Those web sites allow you to publish your work. In this article I would use GitHub as my hosting of a choice, however feel free to use anything you prefer. You can find my repository for this article here.

I will not dive into creation of your repository as it is hosting-specific procedure and may change over time. But fret not as this process is mostly effortless and comes down to creating account (if you have none), pressing button “create repository” and choosing your repository name. The only warning: make sure you create an empty repository. For example, GitHub has an option “init repo with README” which is a nice feature most of the time (do not let it deceive you though, repository always starts from empty directory, GitHub just makes first commit with a README file for you), but right now we need repository to be clean of commits or you might end up having conflicts (more on this later on).

Once we have created our remote repository, we need to tell the git about it. This can be achieved with git remote add command. In my case I ran git remote add origin https://github.com/Binpord/example_repo.git (GitHub actually shows this exact command when I am looking at my empty repository right now). After that you want to publish your changes in the said remote repository. This can be done using git push command. It will request your credentials for the hosting web site. Simply follow its instructions and input your login and password and the push should work. However, it still would not publish your commits, but rather fail with an error:

> git push
fatal: The current branch main has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin main

Frankly its description is self-explanatory. When you first push your branch to a remote repository, you must specify the name of remote (repository can have more than one remote) and the name of the branch on that remote repository. So you run push again but with the required option and it should work fine.

Tip: --set-upstream option can be shortened to -u such that the command becomes git push -u origin main.

Once this is done, git push should work without any additional options:

> echo "lorem ipsum" > yet_another_file.txt
> git add yet_another_file.txt
> git commit -m "modify yet_another_file.txt"
[main ba23d46] modify yet_another_file.txt
 1 file changed, 1 insertion(+)
> git push
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 242 bytes | 242.00 KiB/s, done.
Total 2 (delta 1), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To https://github.com/Binpord/example_repo.git
   37267c6..2f0b9c1  main -> main

Let’s do the responsible thing and add a README to our repository, so that people who stumble upon it might understand what are those three lorem ipsum files all about:

> cat << EOF > README.md
# Example repo

This is an example repository for my article on git.
EOF
> git add README.md
> git commit -m "add a readme"
[main 4325b96] add a readme
 1 file changed, 3 insertions(+)
 create mode 100644 README.md

However, let us hold on to this commit for a short while (meaning that we will not run git push just yet).

Now that we have published our repository, someone might want to get a copy of it. In order to do so, they should use git clone command. In case of my repository command would be git clone https://github.com/Binpord/example_repo.git or git clone https://github.com/Binpord/example_repo.git <directory name> if they wanted it to clone into a directory with a custom name (by default it will take the repository name). For the sake of demonstration I will do just that:

> cd ..
> git clone https://github.com/Binpord/example_repo.git example_repo_clone
Cloning into 'example_repo_clone'...
remote: Enumerating objects: 8, done.
remote: Counting objects: 100% (8/8), done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 8 (delta 1), reused 8 (delta 1), pack-reused 0
Receiving objects: 100% (8/8), done.
Resolving deltas: 100% (1/1), done.
> cd example_repo_clone

And if you were to check history with git log, you would see exactly 3 published commits creating three lorem ipsum files. However, you wouldn’t see the README.md file because we didn’t publish it. Let’s fix that:

> cd ../example_repo
> git push
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 332 bytes | 332.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To https://github.com/Binpord/example_repo.git
   2f0b9c1..232f956  main -> main
> cd ../example_repo_clone

Now we have published our readme and returned back to our clone. It still knows nothing about the last commit which is, if you think about it, a reasonable behavior. We explicitly told our main copy to connect to a remote hosting and publish changes that we made. However, we did nothing to ensure that our clone checked the remote repository state after that. How do we do that? For that we use git pull command:

> git pull
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 1), reused 2 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), 312 bytes | 156.00 KiB/s, done.
From https://github.com/Binpord/example_repo
 + 232f956...4325b96 main       -> origin/main
Updating 2f0b9c1..4325b96
Fast-forward
 README.md | 3 +++
 1 file changed, 3 insertions(+)
 create mode 100644 README.md

And this adds the README.md file to our clone.


Quick note. You might also see the following hint in your git pull output:

hint: Pulling without specifying how to reconcile divergent branches is
hint: discouraged. You can squelch this message by running one of the following
hint: commands sometime before your next pull:
hint: 
hint:   git config pull.rebase false  # merge (the default strategy)
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.

Don’t worry about it for now. Shortly I will tell you more about what merge and rebase are and then you can make your choice.


Thats it for our clone. Let’s delete it and get back to our original repository:

> cd ..
> rm -rf example_repo_clone
> cd example_repo

Knowing clone, push and pull operations is enough to start working with your own repositories, e.g. for storing your dotfiles or your personal projects. However, it is still not enough to start working on a collaborative projects. For that you will need to understand branching.

Branching in git

Branching is a feature that allows you to create a light-weight copy of your repository’s state to start working with it independently of the intermediate changes introduced in the main repository. Branching is very important in git as it allows multiple developers to work with one repository without constantly needing to synchronize their progress. Let’s pretend to be two developers that are making two different tasks.

Branches in git can be created with the git branch <new branch name> command. However, it is not enough to simply create a branch. It is normal (and even intended) use to have many branches in your repository. That is why you need a way to switch between branches. You can do that with git checkout <branch name> command. Important thing to note here is that git branch <new branch name> doesn’t automatically switch you to the newly created branch, so in order to start working on a new branch with name “some_branch” you will need to run two commands: git branch some_branch followed by git checkout some_branch. If this seems like a little bit of extra hustle than I have a solution for you: there is a -b option for the checkout command which will create branch for you and as such two previously mentioned commands will merge into one git checkout -b some_branch. Let’s run it:

> git checkout -b some_branch
Switched to a new branch 'some_branch'

You might be wondering, if we switched to a new branch, what have we switched from? It turns out that with git you are always working on a branch. For that reason git init creates a first branch called “main” and we were working with it ever since. The most attentive readers probably saw “On branch main” in git status output. Well, now you know what was this all about.

Now that we got all this sorted out, let’s do some work in this new branch:

> cat << EOF > foo.py
def foo():
    return "foo"

EOF
> cat << EOF > bar.py
from foo import foo


def bar():
    return foo() + "bar"


if __name__ == "__main__":
    print(bar())

EOF

We created a very useful bar.py module that implements a bar function. And we also implemented a helper function foo in foo.py file. Let’s commit these changes into our branch and publish it in the remote repository (note that once again you must specify the name of remote branch for the first push on a branch):

> git add foo.py bar.py
> git commit -m "implement bar function"
[some_branch 42a548d] implement bar function
 2 files changed, 13 insertions(+)
 create mode 100644 bar.py
 create mode 100644 foo.py
> git push -u origin some_branch
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% (4/4), 427 bytes | 427.00 KiB/s, done.
Total 4 (delta 1), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
remote: 
remote: Create a pull request for 'some_branch' on GitHub by visiting:
remote:      https://github.com/Binpord/example_repo/pull/new/some_branch
remote: 
To https://github.com/Binpord/example_repo.git
 * [new branch]      some_branch -> some_branch
Branch 'some_branch' set up to track remote branch 'some_branch' from 'origin'.

Now that is done, let’s check our main branch (using git checkout main). Here we see neither foo.py nor bar.py files and git log shows us that it is unaware of the latest commit. This is exactly what we wanted when we talked about working on stuff independent of the main branch. And as I told you earlier this is most useful when multiple developers work with our repository simultaneously.

Let’s pretend that we are now another person who is absolutely unaware of the work made in “some_branch”. And we intend to do another (but relatively similar) task. For that we create our own branch “another_branch” and start working:

> git checkout -b another_branch
Switched to a new branch 'another_branch'
> cat << EOF > helper_functions.py
def foo():
    return "foo"

EOF
> cat << EOF > bar.py
from helper_functions import foo


def baz():
    return foo() + "baz"


if __name__ == "__main__":
    print(baz())

EOF
> git add helper_functions.py bar.py
> git commit -m "implement baz function"
[another_branch 1b29f50] implement baz function
 2 files changed, 13 insertions(+)
 create mode 100644 bar.py
 create mode 100644 helper_functions.py
> git push -u origin another_branch
...long push output similar to one in previous example...

Here we created a baz function similar to bar function in some_branch (even in the same file for some reason). It even uses the same foo helper function. However, as we were unaware of “our colleague’s” work in some_branch, we had to re-implement it ourselves. And so we did, but we decided that the appropriate place for it would be a module called helper_functions.py and not the foo.py. After that we pushed our work to remote and voila our task seems to be done.

However, main branch is still absolutely unaware of our work just like it is of the work in some_branch. This was good whilst we were implementing our functions, but now that we are done we want to commit our changes to the main branch. In order to do so, you need to merge branches using git merge <branch name> command from the main branch:

> git checkout main
> git merge some_branch
Updating 4325b96..42a548d
Fast-forward
 bar.py | 10 ++++++++++
 foo.py |  3 +++
 2 files changed, 13 insertions(+)
 create mode 100644 bar.py
 create mode 100644 foo.py

If we look at our log now, we will see the new commit in the main branch.

Quick note: most of the time in real projects you aren’t supposed to simply merge your branch into the main one but rather supposed to create a pull request where other repository maintainers will be able to see your code and ask for changes etc. However, once your request was approved, git will do exactly same merge as we are doing right now so it is still important to understand it.

Let’s merge the second branch:

> git merge another_branch
CONFLICT (add/add): Merge conflict in bar.py
Auto-merging bar.py
Automatic merge failed; fix conflicts and then commit the result.

What happened here? Git found a conflict while merging these branch. And it actually tells you where is the said conflict exactly: both commits added (created) file bar.py. This complaint from git seems reasonable: it doesn’t know how to merge contents of two newly created files into one. And so we should do it ourselves. Let’s look at its current contents:

> cat bar.py
<<<<<<< HEAD
from foo import foo


def bar():
    return foo() + "bar"


if __name__ == "__main__":
    print(bar())
=======
from helper_functions import foo


def baz():
    return foo() + "baz"


if __name__ == "__main__":
    print(baz())
>>>>>>> another_branch

Now git says to us that HEAD (which is git’s way of saying “current state”) has a commit that implements bar function (remember we merged some_branch earlier) and the another_branch that we are merging has a commit that implements baz function. And now its your task to merge this file manually. Important thing to understand is that git doesn’t require you to pick one commit. It just gives you the information on both of them and lets you decide what to do, e.g. you could rewrite this file from scratch or even split it into several files etc.

Arguably the most adequate merge for our bar.py file (from the perspective of the second developer) would be the following:

> cat << EOF > bar.py
import foo
import helper_functions


def bar():
    return foo.foo() + "bar"


def baz():
    return helper_functions.foo() + "baz"


if __name__ == "__main__":
    print(bar())
    print(baz())

EOF

We merged our file. What do we do next? Let’s ask git status:

> git status
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
  (use "git push" to publish your local commits)

You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Changes to be committed:
        new file:   helper_functions.py

Unmerged paths:
  (use "git add <file>..." to mark resolution)
        both added:      bar.py

To mark resolution we need to do git add bar.py:

> git add bar.py
> git status
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
  (use "git push" to publish your local commits)

All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:
        modified:   bar.py
        new file:   helper_functions.py

Now we resolved all the conflicts and to complete the merge we call git commit, which opens up the editor with a default commit message “Merge branch ‘another_branch’”. For our purposes this will do, however, in real life you might want to be a little bit more descriptive as to what has happened.

Both branches are merged. Let’s publish our new state via git push and talk a little bit about our foo function (or rather functions). You might be wondering why did I make two versions of it in two branches and why didn’t I delete one of them during the merge (which would actually be a reasonable thing to do). I made that so that you can see the limitations of the git’s conflict detection: although it detected conflict within one file it won’t go checking your other files’ contents for possible “logical conflicts” such as duplicates. And whilst in this case we had just a one-liner functions which were absolutely same, in real life you may end up with several functions which may have different names and different code that still do exactly same things which is unfortunate. This is not something to be constantly afraid of, but you should be aware of that problem.

Another thing to note is that conflicts aren’t specific to merging branches. You could end up having a conflict if you made some work in your main branch and then try to pull some commits from the remote. And even if you wouldn’t have conflicts, you would end up having constant merge commits merging your local work with remote one. Thats why branching is so useful.

Note how we could merge another_branch despite the fact that main had additional commit compared to the state we branched from. That is what makes merging so powerful and bears the following rule: for every change you should create a branch and merge it once you are done.

Conclusion

Thats it for this tutorial. In conclusion I want to say that this is not in any way a definitive guide on working with git but rather a bunch of things I would like to have been told before I myself started using git. I hope that this is enough for you to start your acquaintance with git. Once you are confident with commands that I presented here, you might want to do your research and find out about other commands and tools that you can use to ease your work.

I tried to keep this article simple and brief as much as possible and didn’t dive into many details. I hope you enjoyed it and thank you for reading it.