Git commands you need to know, simply explained, with hands-on examples!
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
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.
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
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:
- Create a new folder and name it
git-hands-on
- Enter the
git-hands-on
folder and create three files:Loris_file.txt
,Olivias_file.txt
, andmutual_file.txt
- In GitHub, create a new repository, named
_git-hands-on_\
. - Open your project folder in the terminal (open the terminal and
cd
to your project folder). - 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
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
.
-
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. -
git add .
adds all the files to the commit (we’ll talk more about it). -
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! -
git branch -M main
will create a remote branch namedmain
. (What is a branch? You’ll see!) -
git remote add origin
connects the local directory to the remote git repository. -
git push -u origin main
pushes the commit to the remote main branch.
After that, in GitHub, we’ll see this:
Clicking on the commit first commit will show us our text files: Loris_file.txt
, Olivias_file.txt
, and mutual_file.txt
.
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.
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.
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:
- Lori - clones the project
- Olivia - creates her own branch: dev/olivia
- Lori - add, commit, push changes to remote main
- Olivia - pulls Lori's changes from remote main
- Olivia - add, commit, push changes to remote dev/olivia
- Lori and Olivia update the same file
- Olivia - facing and fixing conflicts!
Are you ready to start!?
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:
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.
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:
What do we see here?
-
git branch
shows us the existing branches we have locally. So far we have onlymain
. -
git checkout -b dev/olivia
creates a new branch derived from the current branch we were on ( localmain
, in our case). When I say derived, I mean that whatever change was in localmain
— will be in our new branch as well. -
git branch
shows us bothmain
anddev/olivia
. -
git checkout main
will change the branch we are working on to bemain
. -
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, andgit checkout <NAME>
to switch to a branch.
Notice that the dev/olivia
branch exists only locally on Olivia’s git, not in the remote.
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.
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:
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:
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.
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
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:
So what do we see here?
-
git status
is just to show that nothing happened yet. - changes in
Loris_file.txt
: I added "Here is an update!" at the end of the file. -
git status
shows us there are changes that aren't staged for commit yet, which means we didn't add them withgit add
yet. We can also see here which file has changed:modified: Loris_file.txt
-
git diff Loris_file.txt
shows us what exactly changed in the file. -
git add ./Loris_file.txt
adds the file to the commit. -
git status
after the add, shows us that now there arechanges to be committed
. -
git commit -m ...
commits the changes with the message Lori - updated Loris_file.txt -
git push ...
pushes Lori's commit to the branchorigin main
This is our state after Lori's commit:
- Olivia's Local - Olivia still has the original code in both branches:
main
anddev/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
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.
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:
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?
So, what do we have here?
-
git pull
by itself didn't work, because it doesn't know where to pull from! If we were onmain
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 commandgit branch --set-upstream-to=origin/ dev/olivia
. There is also another way, as you will see next. -
git pull origin main
tells git where to pull from. we can also see here what changed. -
git log
, just to see the difference from the initialgit log
. We'll see it better in the screenshot below.
Here is Olivia's terminal after the pull:
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.
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:
So, what do we have here?
-
status, add, commit ...
we know those already. -
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.
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:
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
.
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:
What do we see here?
-
git checkout main
to switch to the branch main. -
git pull origin dev/olivia
to be up to date with the code. -
git push origin main
to update the remote main branch.
Now when Olivia writes git log
we can see all the branches are up to date.
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:
What do we see here?
-
git checkout dev/olivia
to switch branch. -
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. -
git add, git commit
, we are familiar with those by now. -
git branch --set-upstream-to=origin/dev/olivia
- Before, we were trying to push toorigin/main
from this branch, so this will set the upstream to Olivia's branch. Just to make sure we will push there. - Lastly, the good old familiar
git push
.
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:
So, what do we have here?
-
nodepad .\mutual_file.txt
to show us that Lori made changes to the file. -
git add, git commit
, we are familiar with those by now. -
git status
after each command, I highly recommend you to read the response. - 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:
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:
So, you know what I'm about to ask... What do we have here?
-
git fetch
notifies us about the changes, in which is the branchdev/olivia
appears ("* [new branch] dev/olivia -> origin/dev/olivia"). -
git log
proves that no file was updated - the local main didn't change. -
git pull origin main
does make a change, in a way we haven't seen before: Merge made by the 'recursive' strategy. -
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:
Now that we understand that, let's continue with Lori's 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:
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
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:
How does a conflict look in the code? In our case, like that:
-
<<<<<< 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:
What do we see here?
- 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 ..." - 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 additionalgit add .
andgit status
. Just to be sure. - 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: - push to
origin/dev/olivia
, switch to local main, pull the changes from remoteorigin/dev/olivia
, and push toorigin/main
.
That's it!
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
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
Top comments (0)