DEV Community

Cup of Code
Cup of Code

Posted on • Originally published at cupofcode.blog

A Non-Scary Introduction to The Wondrous World of Git

Git commands you need to know, simply explained, with hands-on examples!

cupofcode_blog_main_picture

It doesn’t matter what's the programming language of your choice, nor which company you work for — you will use Git.

I volunteer in AliceCode and teach programming to teenage girls. Last month I introduced my group to Git, and after seeing them struggle, I noticed all the things a fresh beginner doesn’t understand (yet!)

In this blog post, we will learn the basic git commands and see them used ‘hands-on’. Hands-on means it involves active participation/usage and not just theory.

In contrast to other tutorials, it was important for me to show the flow of the communication with git, and how it looks when the command you thought would work - doesn't.

Here is a Table of Contents, for easy access:

Let's Start With The Basics
Hello world, this is the command-line
Creating the Initial State
Story Time + Hands-on

And here is the list of commands we will discuss:

git init
git clone
git add
git commit
git push
git branch
git checkout
git status
git log
git fetch
Enter fullscreen mode Exit fullscreen mode

Shall we begin?

Let's Start With The Basics

Git is a version control system that lets you manage and keep track of your source code history.

GitHub is a provider of Internet hosting for software development and version control using Git.

You can use Git without GitHub if you have another host, but here we will use GitHub.

Git can get complicated and every command has several command-line flags and options, but today is just about the basics!

Hello world, this is the command-line

The command-line interface (CLI) is a program on your computer that allows you to create and delete files, run programs, and navigate through folders and files. On a Mac, it's called Terminal, and on Windows, it's Command Prompt. It is common to call it a terminal no matter which operating system you're working with, and that is what I do as well.

cupofcode_blog_cli
Now you know which operating system I worked on while creating this tutorial ;)

Command-line flags are a common way to specify options for command-line programs, usually by one hyphen or a double hyphen. For example: git commit -m "message" -> -m is a flag indicating a commit message, and “message” is the message itself.

Things you should know about working with the terminal:

  • mkdir creates a directory (folder)
  • cd <DIRECTORY> enters inside the folder
  • ./ is the current directory, and ../ is the parent directory, so you might see file names like this: ./file_in_current_folder.txt
  • ↑, ↓ on the keyboard will let you navigate previous commands
  • The TAB key will auto-complete names

So, for example:

PS C:\Projects> mkdir git-hands-on
PS C:\Projects> cd gi<TAB> –becomes–> PS C:\Projects> cd git-hands-on
PS C:\Projects\git-hands-on> ↑ –becomes–> PS C:\Projects\git-hands-on> cd git-hands-on
Enter fullscreen mode Exit fullscreen mode

I've created a realistic scenario that you will likely encounter yourself. You can learn by reading only, but I believe it’s more educational (and enjoyable!) to follow along. 

So, if you decided to get your hands dirty, follow the steps in the next section.

 If not, you can jump to the story in the subsequent section.

Creating the Initial State

First, you need to install git (here is a tutorial for Windows) and sign up for GitHub.

After that, you need to create a similar project to the one in my example. In order to do so, you need to:

  1. Create a new folder and name it git-hands-on 
  2. Enter the git-hands-on folder and create three files: Loris_file.txt ,Olivias_file.txt , and mutual_file.txt
  3. In GitHub, create a new repository, named _git-hands-on_\.
  4. Open your project folder in the terminal (open the terminal and cd to your project folder).
  5. Follow the quick setup provided by GitHub: (Don't forget to change <USERNAME> to your GitHub username!)
git init  
git add .  
git commit -m "first commit"  
git branch -M main  
git remote add origin https://github.com/<USERNAME>/git-hands-on.git  
git push -u origin main  
Enter fullscreen mode Exit fullscreen mode

What does this do? I’ll tell you, but each command has its own section in the blog-post, so don’t worry about it! 

Let’s start with the fact that all the git commands will start with git.

  1. git init initializes git on your project. To be clear, this will happen only in the creation of the project, not in cloning! So when you work on an existing project, you won't need to do this step.
  2. git add . adds all the files to the commit (we’ll talk more about it).
  3. git commit -m “first commit" saves the changes locally, with the message “first commit”. What does locally mean? That’s a good question and I will answer it during the hands-on section!
  4. git branch -M main will create a remote branch named main. (What is a branch? You’ll see!)
  5. git remote add origin connects the local directory to the remote git repository. 
  6. git push -u origin main pushes the commit to the remote main branch.

After that, in GitHub, we’ll see this:

cupofcode_blog_1_starting_point

Clicking on the commit first commit will show us our text files: Loris_file.txt, Olivias_file.txt, and mutual_file.txt.

cupofcode_blog_2_files

So you already know our story revolves around Olivia and Lori. I am using one GitHub account, but I want to imitate two different women working from two different computers. To do so, I’ll create two directories (=folders) on my computer, and I’ll start every commit message with the author’s name.

cupofcode_blog_folders

Important note: Even though I will explain every command and response we will encounter here - I highly recommend that you read the full response from each command. Most of the time, the output - especially the error messages - will give you all the information you need!

Now we are ready to start! Wanna hear the story?

Story Time!

Meet Olivia and Lori, who work on the same team.

cupofcode_blog_git_story_intro

Lori and Olivia are starting to work on a project named git-hands-on, that has one branch — main

After cloning the project they’ll start working using different methods: 

Lori will work on her local main branch and push to remote main when she’s done.

Olivia will create her own branch, push that branch to remote as well, and will update it instead of main. When she finishes — Olivia will push the finished code to main

Lori will finish sooner, which means she won’t encounter any conflicts — but Olivia will!

Sounds complicated?! Don’t worry, by the end of this blog-post it will seem simple!

Storyline

So let’s clarify what we will see today:

  1. Lori - clones the project
  2. Olivia - creates her own branch: dev/olivia
  3. Lori - add, commit, push changes to remote main
  4. Olivia - pulls Lori's changes from remote main
  5. Olivia - add, commit, push changes to remote dev/olivia
  6. Lori and Olivia update the same file
  7. Olivia - facing and fixing conflicts!

Are you ready to start!?

cupofcode_blog_you-can-do-it-meme

1. Lori clones the project

git clone https://github.com/<USERNAME>/REPOSITORY

There is a project in GitHub called git-hands-on. Olivia and Lori need to clone it to their computers, so they can start working! Considering that there is only one branch on remote ( main ), the default is that the women will have a local main branch. 

When you clone a repository with git clone, it automatically creates a remote connection called origin pointing back to the cloned repository that is in GitHub. That means that we have a remote version of main branch called origin main. Sometimes I will say remote and sometimes I will say origin - but I mean the same.

This step will look the same for both Olivia and Lori, so let’s see what Lori does:

Loris Terminal

Notice that after cloning Lori went into the git-hands-on folder with the cd .\git-hands-on\ command. With ls we can see the content of the directory, which in our case is the three text files.

Olivia has done the same, and now we have main in remote (in GitHub), and locally on each of the laptops.

cupofcode_blog_git_clone

2. Olivia - creates her own branch: dev/olivia

There are several reasons to create a new branch: Sometimes to separate by the author (as you will see in this case), and sometimes by feature. It’s also good if you want to get someone’s help: They can pull your branch, with all your changes, and run it locally on their computer.

So Olivia wants to create her own branch to work on:

Olivias terminal

What do we see here? 

  1. git branch shows us the existing branches we have locally. So far we have only main
  2. git checkout -b dev/olivia creates a new branch derived from the current branch we were on ( local main, in our case). When I say derived, I mean that whatever change was in local main— will be in our new branch as well.
  3. git branch shows us both main and dev/olivia
  4. git checkout main will change the branch we are working on to be main .
  5. git checkout noSuchBranch will show an error because you are trying to switch to a branch that does not exist. To clarify, git checkout **-b** <NAME> to create a new branch, and git checkout <NAME> to switch to a branch.

Notice that the dev/olivia branch exists only locally on Olivia’s git, not in the remote.

cupofcode_blog_git_checkout

3. Lori - add, commit, push changes to remote main

This step is all about updating the remote main branch. This is a big chunk of the tutorial, and of your work with git. Usually, the cycle is: add, commit, push. add adds which updated will be committed, commit commits the changes, and push pushes the code. add, commit, push happens when we finished working on our task, tested it, and it is good to go!

The meaning behind the commands

It’s kind of like sending a package. You add the items, commit the package to the post office, and push it to the employee.

cupofcode_blog_add_commit_push

It’s a good analogy because you can add multiple items to the same package (git add multiple times before git commit ).

You can also deliver multiple packages in the same visit (git commit multiple times before git push).

It is recommended to add as much as you can to the same commit (within reason). No need to commit every change you make as you go. When you finish your task or reached a meaningful milestone — commit it. 

Let’s say I’ve made changes in three files: a, b and c. It doesn’t matter which of the options I choose:

multiple vs single add

The output would look the same: I've pushed one commit with 3 files.

Also, notice that adding multiple files in the same command is done with space: git add a b c.

When using single/multiple commits, however, the outputs are different:

multiple vs single commit

When you commit each file individually, like in the example on the right side - you'll eventually push three different commits. Multiple commits in the git repository, for no good reason, can get quite messy.

This is also a good place to mention that commit messages are there to help you! So use them wisely and don't write meaningless messages, such as "update" (update where?!), "ifat" (which files? which feature?), etc.

cupofcode_blog_git_meme

If you try to push without committing, it’s like going to the post office to send a package - without the package. If you try committing without adding, it’s like coming to the post office with an empty box — they won’t take it.

Need to see it to believe it? Let’s see what happens when you try to push *nothing* or commit *nothing*:

PS C:\Projects\git-hands-on> git status
On branch main
Your branch is up to date with ‘origin/main’.

nothing to commit, working tree clean

PS C:\Projects\git-hands-on> git push
Everything up-to-date

PS C:\Projects\git-hands-on> git commit -m “empty commit”
On branch main
Your branch is up to date with ‘origin/main’.

nothing to commit, working tree clean
Enter fullscreen mode Exit fullscreen mode

Did you notice a command we haven't mentioned yet? git status !

git status shows us the status of our work: Files that changed, files that were added to the commit, files that were committed, etc.

In our case, we have nothing to commit, working tree clean . When we try to push regardless, it won't do anything because Everything is up-to-date.

Our try to commit *nothing* is also blocked, due to nothing to commit, working tree clean .

Back to the story!

Lori was working on her local main branch and now wants to push her code to the remote main.

Let's look at her terminal:

Loris terminal
Loris terminal

So what do we see here?

  1. git status is just to show that nothing happened yet.
  2. changes in Loris_file.txt: I added "Here is an update!" at the end of the file.
  3. git status shows us there are changes that aren't staged for commit yet, which means we didn't add them with git add yet. We can also see here which file has changed: modified: Loris_file.txt
  4. git diff Loris_file.txt shows us what exactly changed in the file.
  5. git add ./Loris_file.txt adds the file to the commit.
  6. git status after the add, shows us that now there are changes to be committed.
  7. git commit -m ... commits the changes with the message Lori - updated Loris_file.txt
  8. git push ... pushes Lori's commit to the branch origin main

This is our state after Lori's commit:

  • Olivia's Local - Olivia still has the original code in both branches: main and dev/olivia
  • Lori's Local - Lori has her changes on top of the original code
  • Remote - origin main has Lori's changes on top of the original code

cupofcode_blog_git_push

4. Olivia - pulls Lori's changes from remote main

As we've seen in the prior section, Olivia is no longer updated on the latest changes in the code. A fun way to see that is by having both the ladies run the git log command. git log shows commits in various branches in the repository.

Loris log

Let's start with Lori's terminal. We can see that her git is aware of 3 branches: origin/main, origin/HEAD and local main. To be honest, I don't know what the story is with origin/HEAD but it doesn't matter here. I always see it next to origin/main so let's ignore it for now.

We can also see that origin/main is up-to-date with Lori's local main, and that there is a total of 2 commits.

Now, let's look at Olivia's terminal:

Olivias log

Wow wow wow! Did you notice that there is only the initial commit, but it says here that local branch is up to date with the original main???

Unless Lori tells her, the only way for Olivia to know is to try pulling changes from remote. It doesn't mean that Olivia needs to try pull every five minutes. Ideally, Olivia will pull when she finishes working, just before pushing. If Olivia doesn't pull prior to pushing - git will block her from pushing.

Also, notice that on Olivia's terminal we see she has 4 branches, because she created her local dev/olivia.

How can Olivia be made aware of the change in remote?

Olivias terminal

So, what do we have here?

  1. git pull by itself didn't work, because it doesn't know where to pull from! If we were on main it would be obvious, because we cloned the local main from the remote main. dev/olivia branch was 'born' from the local main, and it doesn't have a remote yet. As we can see from the terminal, we can define the remote with the command git branch --set-upstream-to=origin/ dev/olivia . There is also another way, as you will see next.
  2. git pull origin main tells git where to pull from. we can also see here what changed.
  3. git log, just to see the difference from the initial git log. We'll see it better in the screenshot below.

Here is Olivia's terminal after the pull:

Olivias terminal

dev/olivia is the branch we pulled into, so it's up to date with origin/main. The local main, on the other hand, is still on the initial commit.

cupofcode_blog_pull_main

5. Olivia - add, commit, push changes to remote dev/olivia

Now that Olivia is up to date, she keeps working on her file. After finishing, she wants to push it remotely, to have a backup. She can't just do git push because there is no origin/dev/olivia. She needs to create one!

So, how do you create a remote branch? There are two ways to do it. The first is to create an empty remote branch, and then push to it. The second way is to create a commit as usual and create the remote branch with the push - and that's what we will see below:

Olivias terminal

So, what do we have here?

  1. status, add, commit ... we know those already.
  2. git push -u origin dev/olivia the -u flag is for upstream and it's only needed in the first push of that new branch.

git push -u

Pop quiz! What would we see after writing git log in Olivia's terminal? How many commits, and where is each branch?

Let's see if you were right:

olivias terminal

Olivia was working on dev/olivia and pushing to its remote branch, after pulling from origin main.

  • dev/olivia and origin/dev/olivia has 3 commits and is the most updated
  • origin/main has 2 commits - the initial one and Lori's
  • local main has only the initial commit

Fun fact: sometimes git is unexplainable

Well, I'm sure it is explainable, but I don't know why the following thing happened. The good news is - you don't need to understand everything in order to use git ;)

So, I wanted to update origin/main from the local dev/olivia. I thought a simple git push origin main will push to the remote main, but for some reason it tried pushing from main to main: ! [rejected] main -> main.

olivias terminal

So, what can we do if we want to update remote main and we can't do it through local dev/olivia? We can update local main and push from there:

olivias terminal

What do we see here?

  1. git checkout main to switch to the branch main.
  2. git pull origin dev/olivia to be up to date with the code.
  3. git push origin main to update the remote main branch.

cupofcode_blog_olivia_update_origin_main

Now when Olivia writes git log we can see all the branches are up to date.

olivias terminal

6. Lori and Olivia update the same file

You might have noticed that there is a file in the project named mutual_file.txt. In this step, Lori will update it and push to origin/main, and Olivia will update it and push to origin/dev/olivia.

Let's start with Olivia:

olivias terminal

What do we see here?

  1. git checkout dev/olivia to switch branch.
  2. nodepad .\mutual_file.txt opens that file in Notepad, so we can make changes and save. It's just a way to show in the terminal that Olivia made changes to the file.
  3. git add, git commit, we are familiar with those by now.
  4. git branch --set-upstream-to=origin/dev/olivia - Before, we were trying to push to origin/main from this branch, so this will set the upstream to Olivia's branch. Just to make sure we will push there.
  5. Lastly, the good old familiar git push.

cupofcode_blog_olivia_push_mutual_file

In the image above you can see, I added letters to the circles that represent the commits. The pink circles are Lori's commits and the green circles are Olivia's commits. Regarding the letters:

L = an update to Loris_file.txt

O = an update to Olivias_file.txt

m = an update to mutual_file.txt

Olivia's update was pretty simple. With Lori, things are about to get a bit tricky. Let's look at her terminal:

Loris terminal
Loris terminal

So, what do we have here?

  1. nodepad .\mutual_file.txt to show us that Lori made changes to the file.
  2. git add, git commit, we are familiar with those by now.
  3. git status after each command, I highly recommend you to read the response.
  4. Lastly, git log, just to see where we stand.

Wait a minute!! git log shows something weird. We know that origin main has been updated since that commit with Loris_file.txt update. We know Olivia pushed to origin main the change from her branch ("Olivia - modified Olivia's file")! If Lori won’t pull before pushing, and be updated on the latest version - her push will get rejected!!

Here is a visualization of the problem:

cupofcode_git_different_commits_main

This is a great time to meet the last git command for today: git fetch.

git fetch is the command that tells your local git to retrieve the latest meta-data info from the original without doing any file transferring. It's checking to see if there are any changes available. git pull, on the other hand, does that and brings (copies) those changes from the remote repository.

Let's see what happens after Lori fetches the changes:

Loris terminal
Loris terminal

So, you know what I'm about to ask... What do we have here?

  1. git fetch notifies us about the changes, in which is the branch dev/olivia appears ("* [new branch] dev/olivia -> origin/dev/olivia").
  2. git log proves that no file was updated - the local main didn't change.
  3. git pull origin main does make a change, in a way we haven't seen before: Merge made by the 'recursive' strategy.
  4. git log shows us a commit that none of the ladies created: Merge branch 'main' of https://github.com/IfatNeumann/git-hands-on. Why did that happen? Where is Olivia's commit?

Merge branch 'main' commit

Merge branch 'main' of https://github.com/IfatNeumann/git-hands-on

Recursive merge is what happens when we create a commit locally, before pulling changes from origin.

To clarify, Lori already created a commit and did not base it on the latest version of the code. Basing your commit on the latest version of the code is done by pulling from origin prior to committing. It also make sense, because in order to make sure your code is good and doesn't break anything - you need to ensure it works with the rest of the project, as it is now.

If Lori would've tried pushing without pulling first - it would have been rejected. Pulling changes from the origin after committing changes, 'forces' git to organize the commits one after the other even though they originally had the same parent state. By parent state I mean they were both based on the same state of the code.

Let's see how it looks in the repository:

cupofcode_blog_git_pull_origin_main2

Now that we understand that, let's continue with Lori's terminal:

Loris terminal

git status shows us that Lori's local main is ahead of origin/main by 2 commits. They are her original commit (of changing the mutual file) and the recursive merge commit. Let's see how origin/main looks in GitHub:

cupofcode_blog_git_conflict_2

origin/main now has 5 commits. As you can see in the image above, if you look at the code of the two commits, Olivia's commit and the automatic recursive merge commit - you'll see they contain the same changes.

Wow, this was a long section of the story! Here is the state of the branches at this point:

  • Olivia updated the mutual file and pushed the changes to her remote branch
  • Lori updated the mutual file and pushed the changes to main

cupofcode_blog_after_updating_mutual_file

7. Conflicts!

As you saw, both of our ladies modified the same file, and this will for sure create a conflict. It's just a matter of time.

Time is up! Let’s see it happen!

So let’s say Olivia wants to be up-to-date with the latest main version:

Olivias terminal

How does a conflict look in the code? In our case, like that:

cupofcode_blog_19_conflict

  • <<<<<< HEAD marks the code you had in your local repository
  • ====== marks where HEAD ends and the incoming change begins
  • >>>>>> marks the end of the incoming code

If you have multiple occurrences of conflicts in your file, you will have multiple trios of <<<,====,>>>.

When you see a conflict, you need to choose whether you want to keep the version you have, replace it with the incoming change, or keep both. If you work with Visual Studio (an integrated development environment, as seen in the screenshot), you can do it in one click, which makes life easier. In the image above, you can see the options: Accept Current Change | Accept Incoming Change | Accept Both Changes.

In our case, Olivia will choose to keep both.

What do you do after you decide which code to keep? Running the command git status gives us the answer:

Olivias conflict
Olivias conflict
Olivias conflict

What do we see here?

  1. According to git status, we need to fix conflicts and run "git commit", and also that we have Unmerged paths: and should use "git add ..." 
  2. After the command git add . I got the response: Changes to be committed: modified: mutual_file.txt Changes not staged for commit: (use "git add ..." to update what will be committed) modified: mutual_file.txt I found it a bit weird, like it's saying the modified file is to be committed and also not staged for commit, so I did an additional git add . and git status. Just to be sure.
  3. After the commit, I've tried pushing to main but the response was that Everything is up to date. So I did the trick I've done before:
  4. push to origin/dev/olivia, switch to local main, pull the changes from remote origin/dev/olivia, and push to origin/main.

That's it!

cupofcode_blog_is-the-party-over_is-the-party-over

Now you can work with git with no problem!

Today we learned a lot: command-line interface, git, GitHub, how it works, and went over the basic commands:

git init
git clone
git add
git commit
git push
git branch
git checkout
git status
git log
git fetch
Enter fullscreen mode Exit fullscreen mode

We also saw the commands used in real life, and learned how to solve conflicts!

It is ok if you feel a bit overwhelmed. The wonderous world of git is a lot to take in, and mastering it requires practice!

After today, seeing that git is not as scary as it seems, you think you're ready to learn more? Because I have some very useful commands I can share with you in a future blog-post!


I hope you enjoyed reading this article and learned something new! Want to read more? Click here to see what tips Erez Sheiner, Bar Ilan University’s Outstanding Lecturer, has for students!

I would love to hear your thoughts, here are the ways to contact me:

Facebook: https://www.facebook.com/cupofcode.blog/

Instagram: https://www.instagram.com/cupofcode.blog/

Email: cupofcode.blog@gmail.com

[https://cupofcode.blog/](https://cupofcode.blog/)

Top comments (0)