By Kedar Salunkhe
Let me be honest with you for a second.
Most Git articles start with dry definitions, a GitHub logo, and a timeline of Linus Torvalds. You read three paragraphs, your eyes glaze over, and you close the tab. Then you search again, find another article, and repeat the whole cycle.
This isn’t that article.
By the time you’re done here, you’ll actually understand what Git does and why it matters — not just memorize commands that you’ll forget in a week. I’ll use real scenarios, honest explanations, and the kind of walkthrough you’d get from a friend who’s been doing this for a decade and genuinely wants you to understand it.
We’re going to cover everything: what Git is, how it works under the hood, how to use it from scratch, how teams use it in real life, what trips people up, and how to avoid the mistakes that waste hours. This is the full picture, not a highlight reel.
Let’s get into it.
Table of Contents
1. Why Git Even Exists
To understand Git, you have to understand the problem it was built to solve. Because once you feel that problem, Git stops being a confusing tool and starts being an obvious answer.
Before version control systems became widespread, developers did things like this:
- Save a file as
project_final.js - Then
project_final_REAL.js - Then
project_final_REAL_v2_USE_THIS.js - Then
project_backup_Jan14_DONT_DELETE.js - Then
project_Jan14_backup_2_ACTUALLY_USE_THIS_ONE.js
Sound familiar? That system works fine when you’re working alone on something small. But the moment things get even slightly complicated, it falls apart completely.
What happens when you and a colleague are both editing the same file? One of you emails the other a version, they make changes, email it back, you’ve both been working on different copies simultaneously, and now you have to manually figure out how to combine the two. That takes hours. And it’s error-prone. And it’s soul-crushing.
What happens when you make a change, the app breaks, and you have no idea what you changed or when? You’re left digging through files hoping something looks different. Maybe you have a backup from three days ago, but you’ve done a week’s worth of work since then. You’re stuck.
What happens when you want to try a risky experiment — maybe rewrite a big chunk of the codebase — but you’re scared of breaking what’s already working? Most people either don’t try, or they duplicate the entire folder before experimenting, leaving them with a mess of folders with names like project_experimental and project_old_working_version.
These are all real problems that real developers faced every single day before good version control existed. And not just junior developers — everyone dealt with this.
Where Git Came From
Git was created by Linus Torvalds in 2005. Torvalds is the same person who created the Linux operating system kernel — one of the largest and most complex software projects in history, maintained by thousands of contributors around the world simultaneously.
The Linux kernel team was using a version control system called BitKeeper, but when that relationship broke down, Torvalds found himself without a tool that could handle a project of that scale. He evaluated what was available and concluded nothing met his requirements: it had to be fast, it had to handle distributed development properly, and it had to protect against data corruption.
So he built Git himself in about two weeks. Within a month it was managing the Linux kernel’s development. Within a few years it had become the default choice for software projects worldwide. Today, according to the Stack Overflow Developer Survey, over 93% of professional developers use Git. It’s not one option among many — it’s the standard.
2. What Is Git, Really?
Git is a distributed version control system. It tracks changes to files over time, lets you create parallel versions of your project called branches, and enables multiple people to collaborate on the same codebase without overwriting each other’s work.
Let’s break down each part of that definition because each word is doing real work.
Version Control
Version control means tracking the history of your files. Every time you make a meaningful set of changes, you save a snapshot — a commit. Git stores every commit you’ve ever made, in the exact order you made them, forever. You can look at any commit and see exactly what the project looked like at that moment. You can compare any two commits to see exactly what changed. You can go back to any commit if you need to.
Think of it like the undo/redo history in a text editor, but infinitely more powerful. Instead of just the last twenty actions, you have every single change ever made, with a description of what changed and why, going back to the very beginning of the project.
Distributed
The “distributed” part is what makes Git different from older version control systems like SVN or CVS. In those older systems, there was one central server that held the official repository. Developers would check out files, make changes, and check them back in. If the server went down, no one could work. If the server’s hard drive failed, you could lose everything.
Git takes a completely different approach. Every developer who clones a repository gets a full copy of the entire project history on their own machine. Not just the latest version — every single commit, every branch, every tag, going back to day one. This means you can work completely offline, operations like viewing history or switching branches are instant because there’s no network request needed, and if any server fails, every developer’s local copy is a complete backup.
The Time Machine Analogy
The best way to think about Git is as a time machine for your project. At any point, you can jump back to exactly how things looked six months ago. You can create a branch — a parallel timeline — to experiment in, without affecting the main timeline. If the experiment works out, you can merge it back. If it doesn’t, you just delete the branch and nothing was harmed.
That’s an unusually powerful thing to have. Most of the anxiety around coding — the fear of breaking things, the reluctance to try something risky, the stress of coordinating with teammates — is significantly reduced when you know that everything is tracked and almost nothing is permanent.
3. Git vs GitHub vs GitLab — Stop Confusing These
This is the single most common point of confusion for beginners. People use “Git” and “GitHub” interchangeably, and they are not the same thing at all.
Git
Git is the tool itself. It’s free, open-source software that you install on your computer. It runs locally. It has no interface beyond the command line (though third-party apps add graphical interfaces on top of it). It can work completely offline. Git is the engine.
GitHub
GitHub is a website — a cloud hosting platform for Git repositories. You push your local Git repository to GitHub so that it’s backed up online, accessible from anywhere, and shareable with teammates or the public. GitHub adds features on top of Git: Pull Requests for code review, Issues for bug tracking, Actions for automation, and a web interface for browsing code and history. GitHub is owned by Microsoft and was acquired in 2018 for $7.5 billion.
Think of it this way. Git is like Microsoft Word — the software installed on your machine that you use to create documents. GitHub is like Google Drive — the cloud service where you store and share those documents. You can use Word without Google Drive. You can use Git without GitHub.
GitLab
GitLab is a GitHub alternative. It also hosts Git repositories online, but it comes with a more complete built-in DevOps toolchain — things like CI/CD pipelines, container registries, and security scanning are all built in rather than bolted on. Many companies, especially those with strict data privacy requirements, self-host GitLab on their own servers. GitLab competes directly with GitHub for the same market.
Bitbucket
Worth a brief mention: Bitbucket is another Git hosting platform, owned by Atlassian (the company behind Jira and Confluence). It’s popular in enterprises that already use the Atlassian product suite because the integrations are tight. It does the same fundamental job as GitHub and GitLab.
The bottom line: Git is the technology. GitHub, GitLab, and Bitbucket are platforms built on top of that technology. When someone says “put it on GitHub,” they mean “host this Git repository on GitHub’s servers.” The Git part is the same regardless of which platform you use.
4. How Git Actually Works Under the Hood
You don’t need to understand Git’s internals to use it day to day, but a basic mental model of what’s happening will save you a lot of confusion later. When you understand how Git thinks, the commands start to make intuitive sense instead of feeling like magic spells you have to memorize.
Snapshots, Not Differences
Many older version control systems store changes as a series of diffs — essentially recording what changed between each version. Git takes a different approach. Each commit stores a complete snapshot of the entire project at that moment in time. If a file hasn’t changed since the last commit, Git doesn’t store it again — it just points to the identical copy it already has. But conceptually, every commit is a full picture of the project, not a patch on top of the previous picture.
This is why certain operations in Git are so fast and why going back to a previous state is so clean. You’re not replaying a sequence of patches — you’re just loading a snapshot.
The Three States
Every file in a Git project exists in one of three states. This is one of the most important things to understand about Git, and it’s often glossed over in tutorials.
Modified means you’ve changed the file but haven’t told Git about it yet. The changes exist only in your working directory.
Staged means you’ve told Git you want to include this change in the next commit. The change has been added to the staging area, also called the index.
Committed means the change has been permanently stored in Git’s database as part of a commit.
The flow is always: modify a file → stage the change → commit the snapshot. Understanding this flow explains why there are seemingly redundant steps like git add before git commit. The staging area exists so you have granular control over exactly what goes into each commit.
How Git Stores Data
Git stores everything in a hidden folder called .git at the root of your repository. This folder contains the entire history of your project — every commit, every file version, every branch reference. The .git folder is the repository. The files you actually edit are just a working copy checked out from that repository.
Every piece of content in Git is identified by a SHA-1 hash — a 40-character hexadecimal string like a3f9c12b8e1d4f7c2a9b6e3d8f1c5a2b9e4d7f3c. This hash is calculated from the content itself. If even one character changes, the hash changes completely. This is how Git guarantees data integrity — it’s mathematically impossible to alter old data without Git detecting it.
What a Commit Really Is
A commit is a simple object in Git’s database. It contains a pointer to a snapshot of the project, your name and email, a timestamp, the commit message you wrote, and a pointer to the previous commit (the parent commit). That last part — the pointer to the parent — is what creates the chain. Every commit knows where it came from. Follow the chain backwards from any commit and you’ll eventually reach the very first commit, the root of the entire project history.
5. Core Git Concepts You Need to Know
Before we get into commands and practical usage, let’s nail down the vocabulary. These are the terms you’ll encounter constantly and need to have clear in your head.
Repository
A repository, usually shortened to repo, is the project folder that Git is tracking. It contains all your files and the entire history of changes. A local repository lives on your machine. A remote repository lives on a server like GitHub. You’ll typically have both — a local copy you work on and a remote copy you push to.
Working Directory
The working directory is just the current state of your files on disk — what you see when you open your project folder. It’s the actual files you edit. When Git talks about “untracked” or “modified” files, it’s comparing what’s in your working directory to what’s in the last commit.
Staging Area
The staging area, sometimes called the index, is an intermediate zone between your working directory and your commits. When you run git add, you’re moving changes from your working directory into the staging area. When you run git commit, everything in the staging area becomes the new commit.
The staging area gives you the ability to craft precise, logical commits. You might have made five different changes across eight files, but only three of those changes belong together logically. You can stage those three, commit them with a clear message, then stage the rest separately. Your project history ends up clean and readable instead of a jumbled mess of unrelated changes bundled together.
Commit
A commit is a saved snapshot of your staged changes. Once you make a commit, it’s permanent — it will always be there in your history. Every commit has a unique SHA-1 hash ID, your author information, a timestamp, the commit message you provided, and a reference to the parent commit.
The commit message is important and worth treating seriously. A good commit message is a sentence that completes the phrase “If applied, this commit will…” — for example, “Add email validation to the signup form” or “Fix race condition in the payment processing module.” Future you and your teammates will read these messages when trying to understand what happened and why.
Branch
A branch is a lightweight, movable pointer to a commit. The default branch is usually called main (or master in older repositories). When you create a new branch, you’re creating a new pointer that starts at the same commit as your current branch. As you make new commits on that branch, the pointer advances while your original branch stays where it was.
This is how Git lets you work on multiple things simultaneously without them interfering with each other. Each branch is its own line of development. Creating a branch in Git takes milliseconds — it’s just creating a small file with a reference to a commit. This is very different from older version control systems where branching was expensive and avoided.
HEAD
HEAD is a special pointer in Git that tells you where you are right now. It usually points to the tip of your current branch. When you make a new commit, HEAD moves forward to point to that new commit. When you switch branches, HEAD moves to point to the tip of the new branch. If you see “detached HEAD” in a Git message, it means HEAD is pointing directly to a commit instead of to a branch — usually something to be aware of and fix.
Remote
A remote is a version of your repository hosted somewhere else — typically on GitHub, GitLab, or another server. The default remote is usually named origin. When you push commits, you’re sending them from your local repository to the remote. When you pull, you’re fetching changes from the remote and integrating them into your local repository.
Tag
A tag is a named reference to a specific commit, used to mark important points in history — typically release versions. Unlike branches, tags don’t move. When you tag a commit as v1.0.0, that tag will always point to that exact commit. This makes it easy to go back to exactly what your software looked like when you shipped version 1.0.
6. Installing and Setting Up Git
Before you can use Git you need to install it and configure a few basic settings. This takes about five minutes and you only do most of it once.
Installing on macOS
If you have Homebrew installed, which is the recommended package manager for macOS development, run this:
brew install git
Alternatively, Git comes bundled with Xcode Command Line Tools. Running any git command in the terminal on a fresh Mac will prompt you to install them.
Installing on Windows
Download the installer from git-scm.com. The installer comes with Git Bash, a terminal emulator that gives you a Unix-like command line experience on Windows. During installation, use the recommended defaults unless you have a specific reason to change them.
Installing on Linux
On Ubuntu or Debian:
sudo apt-get update
sudo apt-get install git
On Fedora or RHEL:
sudo dnf install git
Verify the Installation
$ git --version
git version 2.43.0
First-Time Configuration
After installing, tell Git who you are. This information gets attached to every commit you make. Run these two commands once:
$ git config --global user.name "Your Name"
$ git config --global user.email "you@example.com"
You can also set your preferred text editor for commit messages. If you use VS Code:
$ git config --global core.editor "code --wait"
To see all your current configuration:
$ git config --list
These settings are stored in a file called .gitconfig in your home directory. You can edit it directly if you want. The --global flag means the settings apply to all repositories on your machine. You can also set per-project config by running the same commands without --global inside a specific project folder.
7. Your First Git Project: Complete Step-by-Step Walkthrough
Enough theory. Let’s actually use Git. Here’s a complete workflow from a blank folder to a proper tracked project with a real commit history.
Starting a New Repository
Create a new folder and initialize a Git repository inside it:
$ mkdir my-project
$ cd my-project
$ git init
Initialized empty Git repository in /my-project/.git/
That last line confirms Git is now tracking this folder. The .git directory has been created. This is where Git stores everything — all commits, all branches, all configuration for this project. Don’t touch it.
Checking the Status
git status is probably the command you’ll run most often. It tells you exactly what state your repository is in — what’s changed, what’s staged, what branch you’re on. Run it now on the empty repo:
$ git status
On branch main
No commits yet
nothing to commit (create/copy files and use "git add" to track)
Creating Your First File
$ echo "# My Project" > README.md
Run git status again:
$ git status
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
README.md
nothing added to commit but untracked files present
Git sees the file but isn’t tracking it yet. It’s in your working directory but not in the staging area.
Staging Your Changes
Add the file to the staging area:
$ git add README.md
Run git status again:
$ git status
On branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: README.md
The file has moved from “Untracked files” to “Changes to be committed.” It’s now in the staging area, ready to be included in the next commit.
Making Your First Commit
$ git commit -m "Initial commit: add README"
[main (root-commit) a3f9c12] Initial commit: add README
1 file changed, 1 insertion(+)
That’s your first commit. Git has permanently recorded this snapshot. That a3f9c12 is the beginning of the commit’s SHA-1 hash — the unique ID for this moment in your project’s history.
Making More Changes
Let’s add some content to the README and create a second file:
$ echo "This project demonstrates Git basics." >> README.md
$ echo "console.log('Hello, Git');" > app.js
Now run git status:
$ git status
On branch main
Changes not staged for commit:
modified: README.md
Untracked files:
app.js
Git sees that README.md was modified and that app.js is a new untracked file. You can see exactly what changed in a tracked file using git diff:
$ git diff
diff --git a/README.md b/README.md
index abc1234..def5678 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,2 @@
# My Project
+This project demonstrates Git basics.
The + at the start of a line means it was added. A - would mean it was removed. Stage and commit both files:
$ git add README.md app.js
$ git commit -m "Add project description and initial app.js"
Viewing the History
$ git log
commit b1e4d90f3c2a8e7d1f4c9b2a5e8d3f6c1a4b7e9 (HEAD -> main)
Author: Your Name <you@example.com>
Date: Tue Mar 18 10:45:00 2025 +0530
Add project description and initial app.js
commit a3f9c12b8e1d4f7c2a9b6e3d8f1c5a2b9e4d7f3
Author: Your Name <you@example.com>
Date: Tue Mar 18 10:30:00 2025 +0530
Initial commit: add README
For a more compact view:
$ git log --oneline
b1e4d90 Add project description and initial app.js
a3f9c12 Initial commit: add README
You now have a real project history. Two snapshots. A trail of everything that’s happened.
8. Branching: The Feature That Changes Everything
If commits are what makes Git useful, branches are what makes Git powerful. Understanding branches well will genuinely change how you think about working on software.
Why Branches Matter
Here’s a concrete scenario. You’re working on a web app. The main branch has stable, working code that’s live on your production server. Your manager asks you to build a new user dashboard. At the same time, a bug report comes in — there’s a broken link on the homepage that needs fixing today.
Without branches, you’d be stuck. You can’t start the dashboard while the code is in a broken state from halfway through that work. You can’t push the bug fix without also pushing your half-finished dashboard.
With branches, this is trivial. You create a branch for the dashboard feature. You create a separate branch for the bug fix. You work on the bug fix, finish it, merge it back to main, and deploy. Meanwhile the dashboard branch has been sitting there untouched, waiting for you. You switch back to it and keep building.
Creating and Switching Branches
To create a new branch and switch to it in one command:
$ git checkout -b feature/user-dashboard
Switched to a new branch 'feature/user-dashboard'
Or using the newer syntax:
$ git switch -c feature/user-dashboard
To see all your branches:
$ git branch
main
* feature/user-dashboard
The asterisk marks the branch you’re currently on. Make some commits on this branch:
$ git add dashboard.js
$ git commit -m "Add basic dashboard layout"
$ git add dashboard.css
$ git commit -m "Style the dashboard components"
Now switch back to main:
$ git checkout main
Your working directory instantly changes. The dashboard files are gone — not deleted, just not present in this branch. They exist safely on the feature/user-dashboard branch. Main still looks exactly as it did before you started.
Creating a Hotfix Branch
$ git checkout -b fix/broken-homepage-link
$ # fix the link in index.html
$ git add index.html
$ git commit -m "Fix broken link in homepage navigation"
$ git checkout main
$ git merge fix/broken-homepage-link
$ git branch -d fix/broken-homepage-link
The bug fix is now in main. You deleted the branch after merging — it’s no longer needed since its commits are part of main. Now you can deploy the fix. The dashboard work is untouched and waiting on its own branch.
Branch Naming Conventions
Most teams use a consistent naming convention for branches. A common pattern is a type prefix followed by a short description:
feature/for new features:feature/user-login,feature/payment-integrationfix/orbugfix/for bug fixes:fix/null-pointer-crash,fix/login-redirectchore/for maintenance tasks:chore/update-dependencies,chore/remove-unused-codedocs/for documentation changes:docs/update-readmehotfix/for urgent production fixes:hotfix/payment-gateway-down
Consistent naming makes it obvious what each branch is for when you’re looking at a list of them, and it helps with automation — many CI/CD systems treat branches differently based on their name prefix.
9. Merging, Rebasing, and Handling Conflicts
Merging
Merging takes the commits from one branch and integrates them into another. The most common pattern is to merge a feature branch back into main when the feature is ready.
$ git checkout main
$ git merge feature/user-dashboard
Updating b1e4d90..f9c3a12
Fast-forward
dashboard.js | 82 ++++++++++++++++++++++++++++++++++++
dashboard.css | 47 +++++++++++++++++++++
In this case Git did a “fast-forward” merge. This happens when main hasn’t moved since you created your feature branch — Git can simply move the main pointer forward to point at your newest feature commit. No new merge commit is needed.
When both branches have new commits since they diverged, Git creates a merge commit — a special commit with two parents that brings both lines of history together.
Understanding Merge Conflicts
A merge conflict happens when Git can’t automatically figure out how to combine two changes. Specifically, it happens when two branches have both modified the same part of the same file in different ways. Git can’t guess which version you want, so it stops and asks you to decide manually.
This sounds frightening. It really isn’t. Here’s what it looks like:
$ git merge feature/new-greeting
Auto-merging greeting.js
CONFLICT (content): Merge conflict in greeting.js
Automatic merge failed; fix conflicts and then commit the result.
Open the conflicted file and you’ll see something like this:
<<<<<<< HEAD
const greeting = 'Hello, world!';
=======
const greeting = 'Hi there, everyone!';
>>>>>>> feature/new-greeting
Git is showing you both versions side by side and asking you to pick. The section between <<<<<<< HEAD and ======= is what’s in your current branch. The section between ======= and >>>>>>> is what came from the branch you’re merging in.
To resolve it, edit the file to what you actually want. Delete all the conflict markers. Save the file. Then stage it and commit:
$ git add greeting.js
$ git commit -m "Merge feature/new-greeting, use combined greeting"
Conflict resolved. If you have many conflicts across many files, editors like VS Code and tools like GitKraken provide visual interfaces that make this process much easier — they show both versions side by side and let you click which one to keep.
Rebasing
Rebasing is another way to integrate changes from one branch into another. Instead of creating a merge commit, rebase replays your commits on top of the target branch, as if you had started your branch from the current tip of main rather than from wherever it was when you branched off.
$ git checkout feature/user-dashboard
$ git rebase main
The result is a cleaner, linear history — no merge commits, just a straight line of commits. The trade-off is that rebase rewrites commit history, which can cause problems if you’ve already pushed those commits to a shared remote branch. As a rule of thumb: never rebase commits that exist on a public or shared branch. Rebase is great for cleaning up your own local feature branch before merging it.
10. Working with Remote Repositories
So far everything we’ve covered has been local — all happening on your own machine. The power of Git in a collaborative context comes from remote repositories. Here’s how to work with them.
Cloning a Repository
To download an existing repository from GitHub to your local machine:
$ git clone https://github.com/username/repository-name.git
Cloning into 'repository-name'...
remote: Enumerating objects: 245, done.
remote: Counting objects: 100% (245/245), done.
Receiving objects: 100% (245/245), done.
This creates a new folder with the repository name, downloads the entire history, and automatically sets up the remote called origin pointing to the GitHub URL.
Connecting a Local Repo to GitHub
If you started a local repo with git init and want to push it to GitHub, first create a new empty repository on GitHub (don’t initialize it with a README), then connect them:
$ git remote add origin https://github.com/username/repository-name.git
$ git branch -M main
$ git push -u origin main
The -u flag sets up tracking, meaning future git push and git pull commands on this branch will automatically use this remote and branch without you having to specify them every time.
Pushing and Pulling
To send your local commits to the remote:
$ git push
To get changes from the remote and merge them into your current branch:
$ git pull
git pull is actually shorthand for two commands: git fetch (download the changes from the remote) followed by git merge (integrate those changes into your local branch). If you want to download changes without automatically merging them, use git fetch — this lets you inspect what came in before deciding how to integrate it.
Pushing a New Branch
When you create a local branch and want to push it to the remote for the first time:
$ git push -u origin feature/user-dashboard
After the first push with -u, you can just use git push from that branch in the future.
Viewing Remotes
$ git remote -v
origin https://github.com/username/repository-name.git (fetch)
origin https://github.com/username/repository-name.git (push)
11. Git in a Team: How Real Workflows Look
Using Git alone on a side project is useful. Using Git on a team is where it becomes truly indispensable. Here’s how professional teams actually work.
The Feature Branch Workflow
The most common workflow in teams of all sizes. The rules are simple: main always contains production-ready code. All development happens on feature branches. Nothing goes into main without a code review via a Pull Request.
Here’s what a typical day looks like for a developer on a team using this workflow:
- Start the day by pulling latest changes:
git pull origin main - Create a branch for what you’re working on:
git checkout -b feature/add-search-functionality - Work on the feature, making small focused commits as you complete each logical piece
- Push the branch to the remote:
git push -u origin feature/add-search-functionality - Open a Pull Request on GitHub, write a description of what you built and why, link any relevant tickets
- A teammate reviews the code, leaves comments, asks questions, suggests improvements
- You address the feedback, push more commits to the same branch, they update the PR automatically
- Once approved, merge the PR into main
- Delete the feature branch — it’s served its purpose
- Pull latest main and start the next task
Pull Requests: The Collaboration Layer
Pull Requests (PRs), also called Merge Requests in GitLab, are not a Git feature — they’re a GitHub feature built on top of Git. A PR is a way of saying “I’ve made these changes on this branch, please review them and consider merging them into main.”
PRs are where code review happens. A teammate looks at your changes, checks that the logic is sound, that edge cases are handled, that naming is clear, that tests pass. They can leave inline comments on specific lines of code. You address the feedback, push new commits, and when everyone is satisfied the PR gets merged.
This process catches bugs before they reach production. It spreads knowledge of the codebase across the team — nobody should be the only person who understands any given part of the system. It maintains code quality over time. And it creates a permanent record of why decisions were made, which is invaluable six months later when someone asks “why is it done this way?”
Git Flow
Git Flow is a more structured branching model, often used by teams that do scheduled releases rather than continuous deployment. It uses two main long-lived branches — main for production code and develop for integration — plus several types of supporting branches: feature branches for development, release branches for preparing releases, and hotfix branches for urgent production patches.
Git Flow works well for products with defined release cycles and multiple versions in support simultaneously. It’s overkill for many web applications that deploy continuously, where the simpler Feature Branch Workflow serves better.
Trunk-Based Development
Trunk-based development is at the other extreme. Developers commit directly to main (the trunk) very frequently — multiple times per day in its purest form — using feature flags to hide unfinished work from users. This approach maximizes integration speed and minimizes the pain of long-running branches diverging from each other. It’s favored by high-velocity teams doing continuous deployment and is the approach used at companies like Google and Facebook at scale.
12. The .gitignore File Explained
Not every file in your project folder should be tracked by Git. There are files you actively don’t want committed to the repository, and the .gitignore file is how you tell Git to ignore them.
What to Put in .gitignore
- Dependencies:
node_modules/,vendor/,.venv/— these can be hundreds of megabytes and can be regenerated from a manifest file likepackage.json - Build output:
dist/,build/,*.min.js— generated files that don’t belong in source control - Environment files:
.env,.env.local— these contain secrets like API keys and database passwords that should never be committed - OS-generated files:
.DS_Storeon macOS,Thumbs.dbon Windows — junk files created by the operating system - Editor-specific files:
.idea/for JetBrains,.vscode/if they contain personal settings (though some VS Code settings are worth sharing) - Log files:
*.log,logs/
Creating a .gitignore File
Create a file called .gitignore in your project root. Here’s an example for a Node.js project:
# Dependencies
node_modules/
# Environment variables
.env
.env.local
.env.production
# Build output
dist/
build/
# Logs
*.log
logs/
# OS files
.DS_Store
Thumbs.db
# Editor
.idea/
*.swp
Lines starting with # are comments. Patterns ending with / match directories. * is a wildcard. The rules apply to the directory containing .gitignore and all subdirectories.
GitHub’s .gitignore Templates
When you create a new repository on GitHub, you can choose a .gitignore template from a dropdown menu. GitHub maintains an excellent collection of templates for every major language and framework at github.com/github/gitignore. Using one of these as a starting point will save you from accidentally committing files that you shouldn’t.
What If You Already Committed a File You Shouldn’t Have?
Adding a file to .gitignore only prevents it from being tracked in the future. If Git is already tracking a file, adding it to .gitignore doesn’t automatically stop tracking it. To stop tracking a file that’s already committed:
$ git rm --cached .env
$ git commit -m "Stop tracking .env file"
The --cached flag removes the file from Git’s tracking but leaves the actual file on your disk. Important: if you accidentally committed a secret like an API key, you need to do more than just remove the file from tracking — the key is still visible in your git history. Rotate the secret immediately, and look into tools like git filter-repo for scrubbing sensitive data from history.
13. Undoing Things in Git
One of the most reassuring things about Git is that almost everything can be undone. The key is knowing which tool to use for which situation.
Undoing Uncommitted Changes
If you’ve made changes to a file and want to throw them away and restore it to the last committed version:
$ git restore app.js
Be careful with this one — it permanently discards your uncommitted changes. They can’t be recovered.
To unstage a file that you’ve added with git add but haven’t committed yet:
$ git restore --staged app.js
Amending the Last Commit
If you just made a commit and realized you forgot to include a file, or you made a typo in the commit message:
# Stage the forgotten file
$ git add forgotten-file.js
# Amend the last commit
$ git commit --amend -m "Correct commit message"
This replaces the last commit with a new one that includes your additions. Only use this on commits that haven’t been pushed to a shared remote — amending rewrites history, which causes problems for anyone who already has the original commit.
Reverting a Commit
git revert is the safe way to undo a commit. It creates a new commit that is the inverse of the commit you’re undoing — effectively canceling out those changes while preserving the full history.
$ git revert a3f9c12
[main f8b2e19] Revert "Add feature that broke everything"
1 file changed, 5 deletions(-)
This is the right tool for undoing commits that are already on a shared branch, because it doesn’t rewrite history — it adds to it.
Resetting to a Previous State
git reset moves the branch pointer back to an earlier commit. It comes in three modes:
git reset --soft HEAD~1moves back one commit but keeps your changes staged. Useful when you want to redo a commit with different content or message.git reset --mixed HEAD~1moves back one commit and unstages the changes, but keeps them in your working directory. This is the default if you don’t specify a mode.git reset --hard HEAD~1moves back one commit and discards all changes completely. Use with extreme caution — this permanently destroys work.
Never use git reset on commits that have been pushed to a shared remote branch. You’ll rewrite history in a way that conflicts with everyone else’s copy of the repository, and it creates a painful mess.
The Reflog: Git’s Safety Net
Git’s reflog is a log of everywhere HEAD has pointed in your local repository. Even if you delete a branch or do a hard reset and think you’ve lost commits, the reflog usually lets you find and recover them. Think of it as Git’s own undo history, beyond even the commit history.
$ git reflog
f8b2e19 HEAD@{0}: revert: Revert "Add feature"
a3f9c12 HEAD@{1}: commit: Add feature
b1e4d90 HEAD@{2}: commit: Previous feature
If you need to recover a commit that seems lost, find its hash in the reflog and create a branch pointing to it: git checkout -b recovery-branch a3f9c12.
14. Git Commands Cheat Sheet
Here’s a reference of the commands you’ll actually use. Not an exhaustive list of every Git command that exists — just the ones that cover the real work.
Setup
git init— Initialize a new Git repositorygit clone <url>— Clone a repository from a remote URLgit config --global user.name "Name"— Set your name globallygit config --global user.email "email"— Set your email globally
Day-to-Day Work
git status— Show the state of the working directory and staging areagit add <file>— Stage a specific filegit add .— Stage all changes in the current directorygit commit -m "message"— Commit staged changes with a messagegit diff— Show unstaged changesgit diff --staged— Show staged changesgit log --oneline— View commit history, one line per commitgit log --oneline --graph— View history as a branch graph
Branching and Merging
git branch— List local branchesgit branch -a— List local and remote branchesgit checkout -b <branch>— Create and switch to a new branchgit checkout <branch>— Switch to an existing branchgit merge <branch>— Merge a branch into the current branchgit rebase <branch>— Rebase current branch onto anothergit branch -d <branch>— Delete a merged branchgit branch -D <branch>— Force-delete a branch (even if unmerged)
Working with Remotes
git remote -v— List remote connectionsgit remote add origin <url>— Add a remotegit push— Push commits to the remotegit push -u origin <branch>— Push and set upstream for new branchgit pull— Fetch and merge from the remotegit fetch— Download from remote without merging
Undoing
git restore <file>— Discard changes to a file in working directorygit restore --staged <file>— Unstage a filegit commit --amend— Modify the last commitgit revert <commit>— Create a new commit that undoes a previous onegit reset --soft HEAD~1— Undo last commit, keep changes stagedgit reset --hard HEAD~1— Undo last commit and discard changes (destructive)git stash— Temporarily save uncommitted workgit stash pop— Restore stashed work
15. Common Mistakes Beginners Make
Working Directly on Main
This is the most common mistake and it creates real problems on teams. Always create a branch for every piece of work, even tiny changes. It’s a habit that costs you about five seconds to establish and saves you from a class of problems entirely. If you’re working alone, it’s still good practice — it keeps main clean and lets you work on multiple things simultaneously without confusion.
Writing Terrible Commit Messages
“Fixed it.” “WIP.” “asdfghjkl.” “changes.” These commit messages are completely useless to anyone reading the history later — including future you. A commit message should clearly state what changed and ideally why. You don’t need an essay. “Fix login redirect loop when session expires” is enough. Your commit history is documentation. Treat it that way.
Making Enormous Commits
A commit that changes 47 files and includes three separate features is not a commit — it’s a mess. Commits should be atomic: one logical change per commit. This makes them easier to review, easier to understand in the history, easier to revert if something goes wrong, and easier to cherry-pick if you need to apply one specific change to another branch. When in doubt, commit smaller and more often.
Forgetting to Pull Before Starting Work
If you’re on a team, other people have been pushing commits since you last pulled. Start every work session with git pull. If you start making changes on an outdated version of main and then try to push later, you’ll run into conflicts that would have been completely avoidable. Pull first, always.
Committing Sensitive Information
API keys, database passwords, private keys, access tokens — these should never be committed to a repository. Not even a private one, because access can change. Use environment variables and a .env file that’s in your .gitignore. If you accidentally commit a secret, treat it as compromised immediately — rotate it even if you immediately remove it from the repo, because it’s visible in the git history.
Force-Pushing to Shared Branches
git push --force rewrites the remote branch history. If you force-push to a branch that teammates have pulled, you’ve rewritten history under their feet and they’ll be in a confusing, broken state. Never force-push to main or any shared branch. It’s fine on your own personal feature branches that no one else has checked out, but treat it as a dangerous command that requires deliberate thought.
Deleting the .git Folder
Sometimes beginners see the hidden .git folder, don’t know what it is, and delete it. This deletes the entire repository history. All your commits, all your branches — gone. The .git folder is the repository. Leave it alone. If you run into a Git problem, the answer is almost never to delete .git. Google the exact error message — there’s almost always a clean solution.
16. Practical Tips That Make You Better at Git
Use git stash When You Need to Context Switch
You’re in the middle of working on something and an urgent issue comes up that requires you to switch branches. But you’ve got uncommitted changes and you’re not at a good stopping point. git stash saves your uncommitted work temporarily so you can switch contexts cleanly:
$ git stash
Saved working directory and index state WIP on feature/dashboard
$ git checkout fix/urgent-bug
# fix the bug, commit it, deploy
$ git checkout feature/dashboard
$ git stash pop
# your work-in-progress is back exactly as you left it
Use git log Effectively
The default git log output is verbose. A few useful variations:
# Compact one-line view
$ git log --oneline
# Visual branch graph
$ git log --oneline --graph --all
# Search commit messages
$ git log --oneline --grep="login"
# Show commits by a specific author
$ git log --oneline --author="Rohan"
# Show changes made to a specific file
$ git log --oneline -- app.js
Use Aliases for Commands You Type Constantly
Git lets you create shortcuts for long commands. Add these to your global config:
$ git config --global alias.st status
$ git config --global alias.co checkout
$ git config --global alias.br branch
$ git config --global alias.last "log -1 HEAD"
$ git config --global alias.lg "log --oneline --graph --all"
Now git st works like git status, git co main works like git checkout main, and so on.
Write a Good .gitconfig
Your ~/.gitconfig file is worth investing a few minutes in. Beyond aliases, you can set the default branch name for new repos to main, configure diff and merge tools, and set up useful defaults. Many experienced developers share their dotfiles publicly on GitHub — browsing a few of these can give you ideas for things to add to your own config.
Use a Good Git GUI When It Helps
The command line is the most powerful way to use Git and understanding it is important. But that doesn’t mean you have to use it for everything. Visual tools shine for certain tasks: visualizing branch histories, staging partial file changes (only committing some of the lines you changed, not all of them), resolving merge conflicts side by side, and doing interactive rebases.
VS Code’s built-in Git interface is solid and good enough for most day-to-day tasks. GitKraken and Sourcetree are popular dedicated GUI clients. Use what makes you most effective — the goal is doing good work, not using the most minimalist toolset.
Commit Often, Perfect Later
A common pattern is to commit frequently during development — even rough, messy commits — and then clean up the history before merging using interactive rebase (git rebase -i). Interactive rebase lets you squash multiple small commits into one clean commit, reorder commits, edit commit messages, and drop commits you don’t want. This gives you the best of both worlds: frequent saves as you work and a clean history in the final merge.
Frequently Asked Questions About Git
Do I need to know Git to get a developer job?
Yes, full stop. Git is non-negotiable in 2025. Every professional development team uses it. Interviewers expect you to understand branching, commits, pull requests, and basic conflict resolution at minimum. It is not a nice-to-have — it’s a baseline skill, like knowing how to use a terminal or read an error message. If you’re building a portfolio or contributing to open source, you’ll need it for that too.
Is Git free to use?
Completely free. Git is open-source software released under the GNU General Public License version 2. GitHub and GitLab are separate services with free tiers. GitHub’s free plan includes unlimited public and private repositories, unlimited collaborators on public repos, and generous limits for private repos that cover most individual developers and small teams with no cost.
How long does it take to learn Git?
You can learn the essential daily commands in a weekend of focused practice. Comfortable, confident usage in a team environment typically comes within two to four weeks of using it on a real project. Deep expertise — understanding Git’s internals, complex rebase strategies, advanced workflows — takes months of consistent use. The good news is that you reach “productive and not confused” surprisingly quickly, and everything after that is incremental improvement.
What is the difference between git fetch and git pull?
git fetch downloads changes from the remote repository but does not integrate them into your local branch. It updates your remote-tracking branches so you can see what’s new, but your working directory is unchanged. git pull does a git fetch followed immediately by a git merge, integrating the remote changes into your current branch automatically. Use git fetch when you want to see what’s changed before deciding how to integrate it. Use git pull when you’re ready to integrate those changes directly.
What is the difference between git merge and git rebase?
Both commands integrate changes from one branch into another but in fundamentally different ways. Merge preserves the complete history exactly as it happened, including when branches diverged and came back together, and creates a merge commit to record that integration point. Rebase rewrites history by replaying your commits on top of the target branch, resulting in a clean linear history with no merge commits. Merge is safer for shared branches because it doesn’t rewrite history. Rebase is useful for cleaning up your own feature branch before a pull request, making the history easier to review. The general rule is: rebase local work, merge shared work.
What is a .gitignore file and why do I need one?
A .gitignore file tells Git which files and folders to ignore completely — never track them, never show them in git status, never include them in commits. You need one because your project folder inevitably contains files that don’t belong in source control: installed dependencies like node_modules, build output, editor configuration files, operating system files, and most importantly, environment files containing secrets like API keys and passwords. Creating a .gitignore before your first commit is one of the best habits you can build. GitHub provides ready-made .gitignore templates for virtually every language and framework.
Can I undo a commit after pushing it to GitHub?
Yes, but you need to be careful about how. The safe approach is git revert, which creates a new commit that undoes the changes from the commit you’re targeting. This preserves the history and doesn’t cause problems for teammates who have already pulled the original commit. What you should avoid is git reset followed by force-pushing on a shared branch, because this rewrites history in a way that conflicts with everyone else’s copy of the repository and can cause serious problems for your team.
What is a Pull Request?
A Pull Request (PR), called a Merge Request in GitLab, is a feature of hosting platforms like GitHub — not a feature of Git itself. It’s a formal request to merge changes from one branch into another, typically from a feature branch into main. Pull Requests provide a structured place for code review: teammates can look at exactly what changed, leave comments on specific lines, ask questions, and request improvements before the code is merged. PRs are how professional teams maintain code quality and spread knowledge across the codebase. Once reviewers approve, the PR is merged and the feature branch is usually deleted.
Should I use the Git command line or a GUI tool?
Both. Learning the command line first is strongly recommended because it gives you a clear understanding of what Git is actually doing, which makes you much better at debugging problems when something goes wrong. But there are tasks where visual tools are genuinely better: resolving merge conflicts with a side-by-side view, staging only specific lines of a changed file, and visualizing complex branch histories. VS Code’s built-in Git integration is excellent for everyday use. GitKraken and Sourcetree are good dedicated clients. Many experienced developers use the command line for most operations and switch to a visual tool when the visual representation is actually useful.
What does “detached HEAD” mean in Git?
A detached HEAD state means that HEAD is pointing directly to a commit rather than to a branch. This usually happens when you checkout a specific commit by its hash or checkout a tag. You can look around and even make commits, but those commits aren’t on any branch — if you switch away from them, they’ll become unreachable. If you want to do work in this state, create a branch first with git checkout -b new-branch-name, which will attach your work to a branch and keep it safe.
Related Resources
External resources worth your time:
- Git Official Documentation — Comprehensive, well-written, and worth reading even as a beginner
- Pro Git (Free Book) — The definitive reference. Free to read online. Chapters 1–3 are essential for beginners.
- GitHub’s Git Handbook — Clean, practical overview from the platform most developers use
- Oh Shit, Git! — The most practical resource for when things go wrong. Honest, funny, and genuinely useful.
- Devops – Your goto reference blogs related to the tech stack in Devops [Kubernetes | Docker | Terraform]
Final Thoughts
Git is one of those tools that feels overwhelming on day one and indispensable by day thirty. The learning curve is real but it’s not steep — it’s just unfamiliar. Once the mental model clicks, it clicks for good.
Here’s the honest summary of what you’ve covered in this guide. Git is a distributed version control system that tracks every change you make to your project, lets you create parallel lines of development with branches, and enables teams to collaborate without stepping on each other’s work. It was built for the scale of the Linux kernel and scales equally well down to your personal side project. Every commit is a permanent snapshot. Every branch is a safe space to experiment. Almost every mistake can be undone.
The commands that will cover 90% of your daily usage are straightforward: git status, git add, git commit, git push, git pull, git checkout -b, and git merge. You’ve seen all of them in detail here. The rest of Git’s capabilities are there when you need them and learnable as that need arises.
The best next step is to actually use it. Pick a project — any project, a script you wrote, a side project you’ve been meaning to start, anything — and initialize a Git repository. Make some commits. Create a branch, do some work on it, merge it back. Push it to GitHub. Once you’ve done this with something real, it will stick in a way that no tutorial can fully replicate.
And when something goes wrong — and at some point something will — don’t panic. Google the exact error message. Visit ohshitgit.com. The Git community has documented solutions to essentially every common problem, and the Git design itself means that almost any mistake is recoverable if you approach it calmly.
The next time you see a senior developer calmly undo a production incident with a few Git commands, you’ll know exactly what’s happening and why it worked.
About the Author
Hi I am Kedar Salunkhe DevOps Engineer by day and Blogger by Night. You can connect with me on Linkedin
Found this useful? Share it with someone who’s just getting started. That’s how these guides reach the people who need them.