Git for absolute beginners: Part 1
By: Krzysztof Czarnecki
Tags: Software Development, Tutorial, Beginner, Git

Photo by Pankaj Patel on Unsplash

My cousin had recently started making an impressive 2D game and while talking I realized that although he breezes through GDScript at an amazing pace, he didn’t have a solution for versioning his code and backing it up somewhere online. I decided to introduce him to Git, which is one of the most used Version Control Systems out there and at some point realized that there isn’t any good resource that would ease-in the concept of what Git really is for. I looked back at when I was just starting to learn Python and had only vague ideas how to use Git and realized that aspiring developers could use some help and high-level explanations (i.e. simple in developers’ jargon).

The buzzword

If you’re a person just venturing now into the world of software development you might have heard Git as one of the buzzwords and frankly, when you write your first few Python scripts or write your first tiny application in Java, you will struggle to understand what the fuss is all about. To give you perspective, imagine now yourself, two, three, five, ten years from now as a battle-hardened software developer.

First imagine you’re working on your magnum opus, alone and in secrecy. Painstakingly putting together bits and pieces of code that will automate creation of drones to let you take over the world. Alas, you realize that one of the files that you just changed, previously contained a large chunk of critical code that you just wanted to compress and optimize but now you don’t even know how to figure out what the original piece did. The original file is lost forever.

Wouldn’t it be great to rewind time? With Git you could…

But now imagine that now, you realize you cannot do it all on your own. It’s just too much work and you need somebody who could help out. You team up with a friend and it goes something like this:

You make an arrangement with your friend, let’s call him John, that every day at 3 PM you will send your copies of the changed files to each other and you will figure out how to integrate the changes. On the first day of work John changes very_important_script.py by adding a function take_over_world that takes up several hundreds of lines. The next day you decide the function is fairly poorly written, some variable names could be changed to make stuff more readable and you also find out that instead of using 3 nested loops you could use just one and save computational time this way. You change take_over_world to your liking and you send the copy of the file to John. John calls you and asks about the file: Where is my triple loop? I have introduced 300 extra lines across this loop that would let us take over the world within a week! You’re baffled. Clearly your solution is better for computational time but John claims that he can make this code reach your common goal way faster than yours. You concede and let him use his copy. The next day you get files from John and you realize that you changed your semi_important_script.py to reflect the changes that you did to the take_over_world function. You would like to get back to the previous version that you had two days ago but you have no way of doing that. You forgot to make a just-in-case copy. You end up writing the same code you've written 2 days ago from scratch, on this occasion introducing several nasty bugs. This back and forth between you and John sending files and discussing changes during long phonecalls goes on and on for a few weeks. You wake up one day and realize that you are back to square one. Or are you? You’re not sure. There is no history you can look into, no record of what happened, no markers in the code who wrote which piece. Your drone automation project to take over the world fails miserably. You still have to pay John for his work.

See that? Working in a team of people can get very hard, even if it’s just you and your friend trying to make things work without the right tools. Tools like Git.

What and why?

Git is a tool that will potentially allow you to alleviate most of the aforementioned headache. It is a version control system tool that lets you:

accurately track changes and the history of your project simplify working with teams of people (record who wrote what and who changed what) revert to older revisions of files when you break something beyond repair create different offshoots of your work (branches and forks, we’ll talk about this later, though not in great detail) potentially back up your code on an associated server, e.g. GitHub (imagine what would happen if you had your magnum opus stored on your drive and your disk got wiped…)

There are of course other tools that do version control. Among some of the alternatives to git are Subversion and Mercurial. I encourage you to read up on these and see which one you like the best, but be aware that a substantial number of companies and open source projects alike is using Git.

The basics

First and foremost: what’s the high-level idea behind Git? When you are starting a new project or have one already that you want to track with Git, you run the git init command in the directory of that project. This will add a special hidden folder .git where Git will store its files allowing it to work properly.

It would make little sense to maintain endless chains of copies of full files. Sometimes these files can get quite large, imagine what would happen when you would have to keep every copy of every incremental change to a file. That’s why Git uses diff s, it records precisely only the parts that were changed with regard to the immediate previous version of each file.

Git does not track all files in your project folder by default. You are expected to tell it which ones’ histories ought to be recorded. You don’t want to include automatically generated files in Git, neither any large binaries that aren’t crucial to your code working properly. So Git by default wants you to tell it explicitly to add files to be tracked. How does this work? Take a glance:

git add

After running git init you will usually want to do git add to add specific files to the so-called staging area or index (these names are used interchangeably). This is a state in Git, where the changes in files are in a way separated from what happens in a working directory. What does that mean? Imagine you git add semi_important_script.py to the index and just keep on working. If you introduce any additional changes to the semi_important_script.py unless you run git add after these changes again, Git will focus only on those that have already been moved to the index.

git commit

The above goes usually hand-in-hand with the next command. While git add lets you specifically select files to be tracked, git commit actually takes all the files from the index and tells Git: these are the changes in these files, add this to the record of project's history. You would usually do something like this: git commit -m "feature: added semi_important_script.py" . After the -m flag comes a message that describes what changes you've made and this message will be added to the history of the repository.

Now, an important thing to note, commits are the unit of Git. Each time you commit something, Git will treat this as a single-step change in the history, so it’s generally recommended to make commits tiny and commit often. Don’t write 300 lines like John from our story and then commit. Write a few that change some small part of the program and commit immediately.

git push

Last but not least in the “saving history” pathway is the git push command. This is the command that will let you send your code to a remote repository, which is a fancy name for a copy of the project that lives on some server, usually with good management tools for developers, like GitHub. git push will only send the files/changes that have been commited before. So you should usually treat this add, commit, push as a repetitive cycle if you are working alone.

git pull

There are two extra commands on the diagram. Let’s talk about pull first. This will come in handy when you're working with teams or simply have to synchronize changes from the server to a copy of the project that lives on a different computer. git pull retrieves any possible changes that might be present on the server and attempts to integrate them to your copy of the project on your own computer.

git clone

This command is used to obtain a fresh full copy of the project from the server and is used in the form: git clone https://example.com/my_repo/ . Unlike pull this creates a separate copy of the project and is useful when you invite someone else to work on your repository or when you want to edit somebody else's code, or when you switch to a new computer instead of copying your projects with flash drives, you can just git clone if your repository on the server is up to date.

Teamwork

I will describe the most basic scenario here, as you will keep on learning new things the more people you have on the team.

First, assumptions: the above picture assumes Alice does not have an existing copy of the project, whereas Bob does. Therefore Alice has to git clone the repository. Next Alice introduces some changes and wants to hand it to Bob. She does the usual git add , git commit and git push to send it to the repository server. If a third person would like to join the party, they could now git clone the repository and they would have the changes made by Alice available to them. Bob needs to do git pull before he can start his work to get all the changes that Alice made. After Bob is done, he also adds the files he changed to the index, commits the changes and pushes them to the repository server.

Conflicts

Now, imagine Alice and Bob are both changing the same bit of code in semi_important_script.py . Bob lives in the Hawaii, Alice in Ohio, so they are not directly aware of what each of them is doing.

What happens when both of them modify the same piece of code but do it in a different way? Imagine the script contains a simple function that both of them modified on their own machines:

# Bob's variant

def add_two_strings(string1, string2):

return str(string1) + str(string2) # Alice's variant

def add_two_strings(string1, string2):

return int(string1) + int(string2)

Those two functions behave completely differently:

The first one concatenates two strings. If you call it with "1" and "2" you will get "12" . It just takes both strings and mushes them together. The second one first converts each string to an integer and then adds these integers. So following the above example it goes like this:

Take "1" and take "2" .

and take . Convert "1" to 1 and "2" to 2 .

to and to . These are now numbers so they will be added in a mathematical sense: 1 + 2 equals 3 .

equals . Return 3 . Notice it's 3 not "3" so this time the function returns a number instead of a string.

As a side note: if you’ve already done some coding in Python you will surely understand why both these functions are crappy: for example notice that when you pass "a" and "b" as arguments to the second one, there will be an error thrown (think about it: how would you convert letters to integers, doesn't make much sense). But for demonstrative purposes, assume these two are legit and let's think about conflict resolution.

A conflict is exactly what we end up with when two changes cannot be merged unambiguously by Git. Notice that while the first function wraps the passed values into a str , the second one uses int . Git won't know which variant to actually keep until you tell it. Git will tell you that a conflict has occurred and in case of Alice and Bob, if either of them try to push the changes to the server and encounter the other variant there, Git will modify the file accordingly to help you resolve the conflict. Imagine Bob pushed first and Alice tries to push her version. Then, the semi_important_script.py will have something like this:

def add_two_strings(string1, string2):

<<<<<<< HEAD

return int(string1) + int(string2)

=======

return str(string1) + str(string2)

>>>>>>> master

Looks scary? Take a look again! The HEAD refers to the bleeding-edge changes that Alice has on her machine. What the whole block says is basically that the changes on the master branch (pushed previously by Bob) are conflicting with the changes in the HEAD .

So how do you resolve a conflict like that? In an ideal scenario Alice should try to communicate with Bob and ask what was the intention with using str or at least investigate the history of commits, to see whether Bob left a message somewhere why he used this approach. Practically to get rid of the conflict Alice has 3 options:

keep her version and override the one Bob wrote:

# Alice deletes the commented-out lines in this scenario def add_two_strings(string1, string2):

# <<<<<<< HEAD

return int(string1) + int(string2)

# =======

# return str(string1) + str(string2)

# >>>>>>> master

keep Bob’s version and discard the one she wrote herself:

# Alice deletes the commented-out lines in this scenario def add_two_strings(string1, string2):

# <<<<<<< HEAD

# return int(string1) + int(string2)

# =======

return str(string1) + str(string2)

# >>>>>>> master

keep both changes (this usually requires some decent refactoring, for example keeping both functions but changing the name of one of them):

# Alice modifies the code, for example like this: def int_add_two_strings(string1, string2):

return int(string1) + int(string2) def str_add_two_strings(string1, string2):

return str(string1) + str(string2)

In this last scenario a little more work would need to be done if the functions had been renamed. Everywhere a function call occurs the right name needs to be used as well. But sometimes it makes sense to keep both variants. Alice could write a function closure, she could connect with Bob and see whether this could be compressed into a single function that makes use of type-checking and has consistent behavior… There are really many, many options here.

Parting message

Hope this little article made you see the power of using Git in your day-to-day newbie coder life. I also hope that the slight intimidation you might have will gradually go away as you dig deeper and deeper into the universe of version control. There are many simplifications I used for this article and while the reality might turn out more complex in the end, as a beginner you should find most of these tips to be sufficient for you to start but I’d love to hear some feedback! Maybe there are topics that you feel should have appeared in this article and you didn’t find them. Maybe you think the weight of this article is too much at once, then feel free to point it out. It might be hard for me to come up with the ultimate set of advice for beginners, as I’m working as a software developer professionally and might discount or overrate the importance of certain elements. This article is essentially a product of the things I would like to have heard myself a few years ago when I was a rookie and the things that my cousin Philip pointed out as a bit confusing when I gave him his first Git lesson. Since no person is made the same, I’d love to see what you guys think should make way into a series of articles for total beginners in Git. And I think one cannot overvalue the fact that knowing Git just makes making software a lot more consistent and structured.