Skip to Content
23 min read

Git Crash Course

Warning

Wow, you just discovered a hidden chapter. This is a draft for one of my another book about programming. I’m keeping it here for due to not being able to come up with a name/domain for that resource.

Alright, let’s quickly cover the 10% of what Git offers that you’ll use 90% of the time.

You’ve probably heard of Git. Maybe you’ve even typed a few git commands into your terminal, copying and pasting from a tutorial, holding your breath and hoping you didn’t just delete a week’s worth of work. We’ve all been there. The thing about Git is that it can feel opaque, even magical, when you first start. It seems to have this arcane set of rules and an incantation for every situation. But I’m going to let you in on a secret: it’s not magic. It’s just a really, really well-thought-out system for keeping track of changes in files. That’s it.

Forget everything you think you know, and forget the fear. By the end of this chapter, you won’t just know the commands; you’ll understand the why behind them. You’ll see your code not as a single, terrifyingly fragile entity, but as a robust history of ideas you can explore, change, and shape with confidence. You’ll stop saving files like main_v2_final_for_real_this_time.js and start working like a professional. Ready? Let’s begin.

Before Git

To truly appreciate what Git does for us, we need to remember what life was like without it. Imagine you’re working on a project. You’ve been coding for a few hours and you’ve got a feature working. It’s good. But now you want to try something new, a different approach that might be way better… or it might completely break everything. What do you do?

The old way was to copy your entire project folder. You’d end up with MyProject/ and MyProject_backup_before_refactor/. A week later, you’d have a dozen of these folders, each a snapshot in time, and you’d have absolutely no idea what the meaningful difference was between backup__final and backup_final_real. If you were working with other people, it was even worse. You’d email zip files back and forth, trying to manually merge changes by staring at two files side-by-side, hoping you didn’t miss a line. It was chaos. It was slow, it was incredibly error-prone, and it made collaboration a nightmare.

This is the fundamental problem that version control systems were created to solve. And Git is, without a doubt, the most successful and widely used version control system on the planet. It gives us a structured, reliable way to record the history of our project, experiment without fear, and collaborate with others seamlessly. It’s the safety net that lets us perform high-wire coding acts.

The Repository

Everything in Git starts with a repository, or “repo” for short. A repository is simply a directory - your project folder - that Git is watching. It contains all of your project files and, most importantly, a hidden sub-directory named .git.

This .git directory is the brain of the operation. It’s where Git stores everything it needs to track the history of your project. It’s a complete database of every change ever made, every version of every file, who made the change, when they made it, and why. The best part? You almost never have to touch anything inside it directly. You just interact with it through Git commands, and it handles all the complex bookkeeping for you.

To turn a regular directory into a Git repository, you use the simplest command of all. You navigate to your project folder in the terminal and run this:

git init

That’s it. This one command creates that magic .git directory and officially tells Git, “Hey, start tracking this folder.” From this moment on, you have a repository. You have a time machine. You’ve just laid the foundation for every powerful feature we’re about to discuss. A pro-tip right from the start: you only run git init once per project, right at the beginning. If you find yourself wanting to run it again, you’re probably thinking about the problem the wrong way.

The Three States

Here is the single most important concept you need to grasp to truly understand Git. Every file in your working directory can be in one of three states: modified, staged, or committed. Let’s break this down, because if you get this, everything else will click into place.

First, you have your Working Directory. This is just your project folder, the files you can see and edit. When you’re coding, adding new files, or deleting old ones, you’re doing it in your working directory. If you change a file that Git is tracking, Git knows about it. It sees the file as modified.

Second, there’s the Staging Area or the index. This is one of Git’s most brilliant features, yet it’s often the most confusing for beginners. Think of the staging area as a draft for your next save point. It’s a space where you can carefully group together a set of related changes before you officially record them in the project’s history. You don’t just save everything that’s changed; you intentionally choose what changes make up the next logical snapshot. You move a file from your working directory to the staging area with the git add command. This act is called “staging.”

Finally, you have the Committed state. This means the changes you staged have been safely stored as a permanent snapshot in the Git repository (that .git directory we talked about). This snapshot, this save point, is called a “commit.” A commit is more than just a copy of the files; it’s a complete picture of your entire project at a specific moment in time. You create a commit from the files in your staging area using the git commit command. Once a change is part of a commit, it’s recorded in your project’s history forever.

So the flow is simple: you make changes in your working directory, you use git add to move the specific changes you want to save into the staging area, and then you use git commit to take everything in the staging area and record it as a permanent commit in your history.

The Basic Workflow - Add and Commit

Let’s make this real. Imagine we have a new project. We’ve run git init, and we create our first file, README.md. Right now, Git knows this file exists, but it isn’t tracking it yet. It’s “untracked.” To check the status of our files, we can use one of the most useful commands you’ll ever learn:

git status

This command is your best friend. It tells you exactly what’s going on: what files are modified, what files are staged, and what files are untracked. Run it often. When you run it now, it will tell you that README.md is an untracked file. Git sees it, but it’s not part of your version history yet.

To start tracking it and prepare it for our first commit, we need to stage it.

git add README.md

This command tells Git, “Okay, I want to include the current version of README.md in my next commit.” If we run git status again, we’ll see that README.md is now listed under “Changes to be committed.” It has moved from the working directory to the staging area.

Now that we’ve staged the change, we’re ready to create our first official save point, our first commit.

git commit -m "Initial commit: Add README file"

Let’s break that down. git commit is the command to create the commit. The -m flag is crucial; it stands for “message.” Every commit needs a message that describes the changes you made. This isn’t optional, and it’s one of the hallmarks of a professional developer. A good commit message explains the why behind the change, not just the what. Our message here is simple and clear.

And that’s it! You’ve just completed the fundamental Git workflow. You modified your project, staged your changes, and committed them to history. Every time you make a meaningful change - fix a bug, add a feature, refactor some code - you’ll repeat this cycle. Modify, add, commit. It will become second nature.

A common gotcha for newcomers is forgetting to git add a file after they’ve modified it. They’ll save the file in their editor, then run git commit, and wonder why their changes weren’t included. Remember, committing only records what’s in the staging area. If you don’t add your changes, they don’t get committed. This is a feature, not a bug! It lets you be incredibly precise about what goes into each commit.

Your Project’s Log

So you’ve made a few commits. How do you see them? Git provides a powerful command to view the history of your project.

git log

Running this will show you a list of all the commits you’ve made, starting with the most recent. Each entry will show you a unique identifier for the commit (called a “hash”), the author, the date, and the commit message you wrote. This log is the story of your project, told one commit at a time. Reading through the log can help you understand how the codebase evolved or track down when a particular bug was introduced.

The default git log can be a bit verbose. A pro-tip is to use a more compact format. I personally use this alias all the time:

Bash

git log --oneline --graph --all

--oneline shows each commit on a single line. --graph draws a cool little ASCII art diagram of the branches (we’ll get to those soon). --all shows commits from every branch, not just your current one. It gives you a fantastic, high-level overview of your entire project history.

Making it even better (optional)

While the command above is a great start, we can make it even more insightful by adding custom formatting. This next version is what I use daily. It not only shows the commit history but also who made the change, when they made it, and what files they touched; all in a beautifully colored, single line.

git log --color=always --name-only --pretty="format:%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset" --all | awk 'BEGIN{RS=""; FS="\n"}{commit=$1; files=""; for(i=2; i<=NF; i++){if($i!=""){files=files?(files ", " $i):$i;}} print commit (files?" ["files"]":"")}'

That command will be a nightmare to type, so you’ll need to save it as a Git alias. An alias is a shortcut you create for a longer command. Let’s name our new, powerful log command hist (short for history).

Just run this one command in your terminal to set it up -

git config --global alias.hist '!git log --color=always --name-only --pretty="format:%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset" --all | awk '\''BEGIN{RS=""; FS="\n"}{commit=$1; files=""; for(i=2; i<=NF; i++){if($i!=""){files=files?(files ", " $i):$i;}} print commit (files?" ["files"]":"")}'\'''

From now on, you can just type git hist to see the super-powered log. Here’s how it looks like when I run this command in one of my repositories.

Git history command output

The real power of this command is the sheer amount of context it provides at a glance. Once you start using git hist, you’ll wonder how you ever lived without it. It turns your Git history from a simple list of commits into a rich, scannable story of your project’s evolution.

The Power of Snapshots

It’s really important to understand what a commit actually is. A common misconception is that Git stores changes or “diffs” between files. This isn’t quite right. The reality is simpler and more powerful.

Each commit is a snapshot of your entire project at that moment in time.

When you commit, Git doesn’t just record what changed in README.md. It essentially takes a picture of what every single file in your project looked like at that instant and stores it. For files that haven’t changed since the last commit, Git is incredibly efficient; it doesn’t store a new copy but simply includes a pointer to the identical version it already has stored.

This snapshot approach is why Git is so fast and powerful. It makes operations like switching between different versions of your project incredibly quick, because Git doesn’t have to rebuild a file by applying a long series of changes. It can just grab the exact snapshot of the file it needs directly from its database. Understanding this “snapshot” model will make advanced Git concepts much easier to grasp later on.

Branching

Okay, we’ve covered the basics of saving our work. Now let’s talk about the feature that truly makes Git an indispensable tool for modern software development: branching.

Up until now, we’ve been working on a single timeline, a single branch. By default, this branch is called main (or sometimes master in older repositories). Think of it as the main trunk of your project’s history tree.

But what if you want to work on a new feature? You could just start adding commits to main, but that’s risky. What if the feature takes a week to finish? During that time, your main branch will be in a broken, incomplete state. What if you need to fix an urgent bug in the production code while you’re in the middle of this new feature? You’d have to somehow undo your partial changes, fix the bug, and then try to re-apply them. It’s messy.

Branches are the solution. A branch is essentially a movable pointer to a commit. When you create a new branch, all you’re doing is creating a new pointer. It’s an incredibly lightweight operation. You’re not copying your entire project folder. You’re just saying, “I want to start a new line of development from this point.”

Let’s say we want to build a new user authentication feature. Instead of working on main, we’ll create a dedicated branch for it.

git branch feature/user-auth

This command creates a new branch named feature/user-auth. It’s a good practice to name branches descriptively. Using a prefix like feature/ or bugfix/ helps keep things organized. When you run this, you’re still on the main branch. You’ve created the new branch, but you haven’t switched to it yet. To do that, you use the checkout command.

git checkout feature/user-auth

This command switches your working directory to reflect the state of the feature/user-auth branch. Right now, it looks exactly the same as main because the feature/user-auth pointer is pointing at the same commit as the main pointer. But now, when we make new commits, the feature/user-auth pointer will move forward, while the main pointer stays put.

A handy shortcut to create and switch to a new branch in one go is:

git checkout -b feature/user-auth

The -b flag tells checkout to create a new branch before switching. This is what you’ll use 99% of the time.

Now we’re on our new branch. We can go wild. We can add files, delete files, refactor everything. We can make a hundred commits. All of this work is completely isolated from the main branch. main remains stable and untouched, representing the last known good state of our project. This is freeing. It allows you to experiment with zero risk. If the feature turns out to be a terrible idea, you can just delete the branch and it’s like it never happened.

Important

The git checkout command is a classic, but it’s a bit of an overachiever. It does a bunch of different jobs. To make life simpler, Git introduced a new command called git switch. Its only job is to switch you from one branch to another. That’s it! This makes it much easier to use and helps you avoid accidents. So, instead of git checkout feature/user-auth, you can just use git switch feature/user-auth. To create a new branch and switch to it at the same time, you can use git switch -c feature/user-auth. It’s a great new habit to get into!

Merging

You’ve finished your feature on the feature/user-auth branch. It’s tested, it works perfectly, and you’re ready to incorporate it into the main project. This process is called merging. You want to merge the changes from your feature branch back into your main branch.

The process is straightforward. First, you need to switch back to the branch you want to merge into. In this case, that’s main.

git checkout main

Now, you run the merge command, telling it which branch you want to merge from.

git merge feature/user-auth

Git will now look at the work you did on feature/user-auth and the state of main, and it will combine them. In the simplest case, main hasn’t had any new commits since you created your feature branch. This is called a “fast-forward” merge. Git just moves the main branch pointer forward to point to the same commit as your feature branch. It’s clean and simple.

Merge Conflicts

Sometimes, things aren’t so simple. Imagine that while you were working on your feature branch, another developer made a change to the main branch. Maybe they fixed a bug and committed it. Now, the histories of the two branches have diverged. Both branches have new commits that the other doesn’t have.

When you try to merge feature/user-auth into main, Git will still try to combine the work. It will create a new “merge commit” that has two parents: one from main and one from your feature branch, uniting the two separate histories.

Most of the time, Git is smart enough to figure this out on its own. If you changed a file named auth.js and the other developer changed styles.css, there’s no problem. Git will combine the changes without issue.

But what if you and the other developer both changed the exact same line in the exact same file? Git is powerful, but it can’t read your mind. It doesn’t know which version is the correct one. This is a merge conflict, and it’s something that used to send junior developers into a panic. But it doesn’t have to be scary.

When a merge conflict happens, Git will stop the merge process and tell you which file (or files) have a conflict. If you open the conflicted file, you’ll see something that looks a bit strange. Git will have edited the file to show you both versions of the conflicting code, marked with special indicators.

<<<<<<< HEAD // Code from your current branch (main) ======= // Code from the branch you're merging in (feature/user-auth) >>>>>>> feature/user-auth

HEAD is just a special pointer in Git that always refers to the branch you are currently on. So <<<<<<< HEAD marks the beginning of the conflicting code from main, ======= separates the two versions, and >>>>>>> feature/user-auth marks the end of the code from your feature branch.

Your job is simple: you have to resolve this conflict. You edit the file, remove the markers Git added, and decide what the final, correct version of the code should be. Maybe you want to keep your version, maybe the other person’s version, or maybe some combination of both. You are the human, and you have to make the final decision.

Once you’ve edited the file and saved it, you need to tell Git that you’ve resolved the conflict. You do this by staging the file, just like you would with any other change.

git add conflicted_file.js

After you’ve done this for all the conflicted files, you can complete the merge by running git commit. Git will see that you’re in the middle of a merge and will pre-populate the commit message for you. You can just save it, and the merge is complete. That’s it. Merge conflicts are not a sign of failure; they are a normal and healthy part of collaborative development. The key is to stay calm, read what Git is telling you, and resolve the changes one by one.

Remote Repositories

So far, everything we’ve done has been on our local machine, inside that .git directory. This is great for solo work, but the real power of Git shines when you collaborate with a team. To do that, we need a central place to share our work. This is where remote repositories come in.

A remote is just a version of your repository that is hosted somewhere else, usually on a service like GitHub, GitLab, or Bitbucket. It’s a common ground where everyone on the team can push their changes to and pull others’ changes from.

When you’re starting a new project that’s already hosted on, say, GitHub, you don’t start with git init. Instead, you “clone” the remote repository.

git clone <repository_url>

This command does two important things. First, it downloads a complete copy of the entire repository - every file, every branch, and every single commit in the project’s history - to your local machine. Second, it automatically sets up a connection to the remote repository for you. By default, this connection is named origin. origin is just a conventional alias for the URL you cloned from. It’s where you’ll push your changes and fetch updates.

Fetch, Pull, and Push

Once you’ve cloned a repo, you’ll be doing three main things to interact with the remote: push, fetch, and pull.

When you’ve made some commits on your local machine and you want to share them with your team, you use the push command.

git push origin main

This command says, “Take my main branch and upload all the commits that the origin remote doesn’t have yet.” Git will transfer your commits over the network, and now your teammates can see your work.

But what about getting updates from your teammates? This is where things can get a little confusing for newcomers, because there are two commands: fetch and pull. Let’s clarify the difference, because it’s important.

The git fetch command is a “safe” way to get updates. It connects to the remote repository and downloads all the new data - new branches, new commits - but it does not change any of your local files or branches. It just updates your local .git directory with the latest information from the remote. You can then see what has changed on the remote by inspecting the remote-tracking branches, like origin/main.

git fetch origin

After fetching, your local main branch is still where you left it, but origin/main has been updated to point to the latest commit from the remote. This gives you a chance to review the changes before you decide to integrate them into your own work.

The git pull command is a bit more aggressive. It’s essentially two commands in one.

git pull origin main

A git pull is really just a git fetch followed immediately by a git merge. It fetches the latest changes from origin/main and then immediately tries to merge them into your local main branch. This is convenient, and it’s what you’ll often do, but it’s important to understand what’s happening under the hood. It’s fetching and merging in a single step.

A pro-tip for a cleaner history is to configure pull to use “rebase” instead of “merge.” We won’t go deep into rebasing here, but in short, it replays your local commits on top of the fetched changes, creating a cleaner, linear history instead of a merge commit. You can set this as a default with git config --global pull.rebase true. It’s a slightly more advanced workflow, but one worth learning as you grow.

”Oops, I Didn’t Mean to Do That”

One of the best things about Git is that it’s very hard to truly lose work. It’s designed to be a safety net. Here are a few commands to help you recover when you make a mistake.

Let’s say you just made a commit, and you immediately realize you forgot to add a file, or you made a typo in the commit message. You don’t need to create a whole new commit to fix it. You can amend the previous one. First, stage the file you forgot, or just fix what you need to fix. Then, you can commit with the --amend flag.

git add forgotten_file.js git commit --amend

This will open up your text editor and let you edit the last commit message. When you save and close it, Git will replace the last commit with a new one that includes your latest changes and the new message. One huge warning here: you should never amend a commit that you have already pushed to a remote repository and shared with others. Amending rewrites history, and rewriting shared history can cause massive problems for your collaborators. Use it only for commits that still exist only on your local machine.

What if you made a commit that you now realize was a terrible idea, and you want to undo it? If it’s a commit you’ve already pushed, you can’t just delete it. Instead, you create a new commit that does the exact opposite. This is called “reverting.”

git revert <commit_hash>

You can find the hash of the bad commit from git log. When you run git revert, Git will create a brand new commit that undoes the changes from the commit you specified. This is the safe way to undo things in a shared history, because you’re not rewriting the past; you’re adding to it. You’re transparently saying, “Oops, let’s undo that last change.”

Finally, there’s the powerful but dangerous git reset command. This command can move branch pointers around and even modify your staging area and working directory. The most common “safe” use is for unstaging a file. If you git add a file by accident and don’t want it to be in the next commit, you can unstage it.

git reset HEAD some_file.txt

This removes the file from the staging area but leaves your changes in the working directory. There are more powerful forms of git reset (like --hard) that can actually delete your work, so be very careful with it. Always run git status before and after a reset to make sure you understand what happened. My advice is to stick with revert for undoing public commits and amend for fixing private ones until you’re very comfortable with how reset works.

The Art of a Good Commit Message

I want to end with something that isn’t a command, but a craft: writing good commit messages. This is a skill that separates junior developers from senior ones. A commit is a snapshot of the code, but the message is the story behind that snapshot. It’s a communication tool for your future self and for your teammates.

A good commit message has two parts: a short, descriptive subject line, and a more detailed body.

The subject line should be 50 characters or less and should complete the sentence: “If applied, this commit will…” For example, “Add user login endpoint” or “Fix off-by-one error in pagination.” It should be concise and in the imperative mood.

The body of the commit message is optional but incredibly useful for more complex changes. Here, you explain the why of the change. What was the problem? How did you solve it? Why did you choose this particular solution? This context is invaluable when someone (possibly you, six months from now) is trying to understand why a piece of code exists.

git commit

If you run git commit without the -m flag, Git will open a text editor for you to write a longer message. The first line is the subject, and after a blank line, you can write the body. Take the extra thirty seconds to write a good message. It will pay for itself a hundred times over.

Git is a deep and powerful tool, and we’ve only scratched the surface. But what we’ve covered here - the core workflow, branching, merging, and collaborating - is the 90% you’ll use every single day. The rest you can learn as you go.

Don’t be afraid to experiment. Create a test repository and try to break things. Make branches, cause merge conflicts on purpose, and fix them. The more you use Git, the more it will become an extension of your thought process. It’s not just a tool for saving files; it’s a tool for thinking about, structuring, and communicating the evolution of your software. Welcome to the club.