Git is the version control system that runs the software industry. Created by Linus Torvalds in 2005 to manage Linux kernel development, it has become the universal standard for tracking code changes, collaborating with other developers, and maintaining a complete history of every decision made in a project. Whether you are a solo developer working on a side project or part of a team shipping production software, Git is a foundational skill.
This tutorial covers Git from first principles through professional workflows, with commands you can run in your terminal right now.
Why Version Control Matters
Without version control, developers rely on manual backups — folders named project-v2-final, project-v2-final-FIXED, project-v2-final-FIXED-2. This approach fails immediately once two people need to work on the same code, and it fails silently when a single developer needs to undo changes made three weeks ago.
Git solves these problems by maintaining a complete, searchable history of every change. Each commit is a snapshot of your entire project at a specific point in time. You can jump to any snapshot, compare any two versions, and merge changes from multiple developers without overwriting anyone’s work.
Installing and Configuring Git
Git comes pre-installed on most macOS and Linux systems. On Windows, download it from git-scm.com or install it through the Windows Package Manager. After installation, configure your identity — this information appears in every commit you create:
# Set your identity (required before your first commit)
git config --global user.name "Your Name"
git config --global user.email "your.email@example.com"
# Set the default branch name to 'main'
git config --global init.defaultBranch main
# Enable colored output for readability
git config --global color.ui auto
# Set VS Code as the default editor for commit messages
git config --global core.editor "code --wait"
# Verify your configuration
git config --list
Creating Your First Repository
A Git repository is a directory that Git tracks. You create one by running git init inside any folder:
# Create a new project directory and initialize Git
mkdir my-project
cd my-project
git init
# Git creates a hidden .git directory that stores all version history
ls -la
# .git/ — this is the repository database
The .git directory contains the entire history of your project — every commit, branch, and tag. The files in your project directory are called the working tree. Understanding this distinction between the repository (inside .git) and the working tree (your actual files) is key to understanding Git.
The Three Areas: Working Tree, Staging, and Repository
Git uses three areas to manage changes, and understanding them prevents most confusion beginners encounter:
- Working tree — Your actual files on disk. Edit them normally with any code editor
- Staging area (index) — A preview of your next commit. You choose which changes to include
- Repository — The permanent history. Once committed, snapshots are nearly impossible to lose
# Create a file
echo "<h1>Hello World</h1>" > index.html
# Check the status — Git sees an untracked file
git status
# Untracked files: index.html
# Stage the file (add it to the staging area)
git add index.html
# Check status again — file is staged and ready to commit
git status
# Changes to be committed: new file: index.html
# Commit the staged changes with a descriptive message
git commit -m "Add homepage with heading"
# View the commit history
git log --oneline
# a1b2c3d Add homepage with heading
Staging Specific Changes
One of Git’s most useful features is the ability to stage specific files or even specific lines within a file. This lets you create focused commits that each represent a single logical change:
# Stage specific files
git add index.html style.css
# Stage all files in a directory
git add src/
# Stage all modified and new files (use with caution)
git add .
# See what's staged vs unstaged
git diff --staged # shows staged changes
git diff # shows unstaged changes
# Unstage a file without losing changes
git restore --staged index.html
Writing Good Commit Messages
Commit messages are documentation. Six months from now, a clear commit message is the difference between understanding why a change was made and having to reverse-engineer the intent from the code diff.
# Single-line message for small changes
git commit -m "Fix navigation links on mobile viewport"
# Multi-line message for larger changes
git commit -m "Add user authentication system
Implement JWT-based authentication with refresh tokens.
Includes login, logout, and password reset endpoints.
Session tokens expire after 24 hours.
Closes #42"
Follow these conventions: use the imperative mood (“Add feature” not “Added feature”), keep the first line under 72 characters, and explain why the change was made — the diff already shows what changed.
Branching
Branches are Git’s defining feature. A branch is a lightweight pointer to a commit, and creating one takes milliseconds regardless of project size. Branches let you work on features, fixes, and experiments in isolation without affecting the main codebase.
# List all branches (* marks the current branch)
git branch
# * main
# Create a new branch and switch to it
git checkout -b feature/user-login
# Equivalent modern syntax (Git 2.23+)
git switch -c feature/user-login
# Make changes and commit on the feature branch
echo "login form markup" > login.html
git add login.html
git commit -m "Add login form markup"
# Switch back to main
git switch main
# The login.html file disappears — it only exists on the feature branch
ls
# index.html style.css
Branch Naming Conventions
Most teams follow a pattern that communicates the purpose of each branch:
feature/user-authentication— New functionalityfix/login-redirect-loop— Bug fixeshotfix/security-patch— Urgent production fixesrefactor/extract-api-client— Code restructuringdocs/api-reference— Documentation updates
Merging
Merging combines changes from one branch into another. Git handles most merges automatically by comparing the common ancestor of both branches with their current states:
# Switch to the branch you want to merge INTO
git switch main
# Merge the feature branch into main
git merge feature/user-login
# Merge made by the 'ort' strategy.
# Delete the feature branch after merging (optional but recommended)
git branch -d feature/user-login
Handling Merge Conflicts
Conflicts occur when two branches modify the same lines in the same file. Git marks the conflicting sections and lets you resolve them manually:
<!-- Git shows both versions with conflict markers -->
<<<<<<< HEAD
<h1>Welcome to Our Platform</h1>
=======
<h1>Welcome to the App</h1>
>>>>>>> feature/rebrand
# After editing the file to resolve the conflict:
git add index.html
git commit -m "Merge feature/rebrand, keep updated heading"
Modern code editors provide visual merge conflict resolution with buttons to accept one side, the other, or both. VS Code highlights conflicts in color and lets you resolve them with a single click.
Rebasing
Rebasing rewrites commit history by moving a branch’s commits onto a new base. It produces a linear history that is easier to read than merge commits, but it changes commit hashes and should only be used on branches that have not been shared with others:
# On your feature branch, rebase onto the latest main
git switch feature/dashboard
git rebase main
# If conflicts occur, resolve them and continue
git add .
git rebase --continue
# Or abort the rebase entirely
git rebase --abort
The general rule: rebase your own feature branches to keep them current with main, but use merge for integrating completed features into shared branches.
Working with Remote Repositories
Remote repositories on GitHub, GitLab, or Bitbucket enable collaboration. The origin remote is the default name for the repository you cloned from:
# Clone an existing repository
git clone https://github.com/username/project.git
cd project
# View configured remotes
git remote -v
# Push your branch to the remote
git push -u origin feature/user-login
# Fetch updates from the remote without merging
git fetch origin
# Pull updates (fetch + merge)
git pull origin main
# Push changes after committing
git push
The Pull Request Workflow
The standard collaboration workflow on GitHub combines Git branching with code review. This is how most professional teams ship code:
# 1. Create a feature branch from an up-to-date main
git switch main
git pull origin main
git switch -c feature/search-functionality
# 2. Develop and commit your changes
git add src/search.ts src/search.test.ts
git commit -m "Add search functionality with fuzzy matching"
# 3. Push the branch to GitHub
git push -u origin feature/search-functionality
# 4. Open a pull request on GitHub for code review
# 5. Address review feedback with additional commits
# 6. Once approved, merge the PR on GitHub
# 7. Delete the remote branch and pull the updated main
git switch main
git pull origin main
git branch -d feature/search-functionality
Essential Git Commands Reference
Beyond the basics, these commands handle situations that come up regularly in development:
# Temporarily save uncommitted changes
git stash
git stash pop # restore the most recent stash
git stash list # view all stashes
# View what changed in a specific commit
git show a1b2c3d
# Undo the last commit but keep changes staged
git reset --soft HEAD~1
# Discard changes in a specific file
git restore index.html
# Search commit messages
git log --grep="login" --oneline
# View who changed each line of a file
git blame src/auth.ts
# Create a visual log with branch structure
git log --oneline --graph --all
# Cherry-pick a specific commit from another branch
git cherry-pick a1b2c3d
# Tag a release
git tag -a v1.0.0 -m "Release version 1.0.0"
git push origin v1.0.0
The .gitignore File
Not everything belongs in version control. Dependencies, build artifacts, environment variables, and editor configuration should be excluded. Create a .gitignore file in your project root:
# Dependencies
node_modules/
vendor/
# Build output
dist/
build/
.next/
# Environment variables (never commit secrets)
.env
.env.local
.env.production
# OS files
.DS_Store
Thumbs.db
# Editor directories
.idea/
.vscode/settings.json
# Logs
*.log
npm-debug.log*
Git in Professional Workflows
In production environments, Git integrates with CI/CD pipelines, automated testing, and deployment systems. Pushing to a branch triggers automated tests through GitHub Actions or similar platforms. Merging to main triggers deployment to staging or production. This automation relies on disciplined Git usage — clean commit history, meaningful branch names, and consistent workflows. Teams working with modern CSS and responsive design systems benefit especially from feature branches, as visual changes can be reviewed in isolation through preview deployments.
Tools like performance monitoring platforms can tie metrics back to specific commits, making it possible to identify exactly which change caused a performance regression. Combined with a modern framework that supports preview deployments, every pull request can be reviewed in a live environment before merging.
For project coordination beyond Git, tools like Taskee connect task tracking with branch and pull request workflows, giving teams visibility into which features are in progress, in review, and deployed.
Frequently Asked Questions
What is the difference between git pull and git fetch?
git fetch downloads changes from the remote repository without modifying your working directory or current branch. git pull runs git fetch followed by git merge, which immediately integrates the remote changes into your current branch. Use fetch when you want to see what changed before merging, and pull when you are ready to integrate.
How do I undo a commit that has already been pushed?
Use git revert <commit-hash> to create a new commit that undoes the changes from a specific commit. Unlike git reset, revert is safe to use on shared branches because it does not rewrite history. The original commit remains in the log, and the revert commit explicitly documents what was undone and why.
Should I use merge or rebase?
Use rebase to keep your feature branches up to date with main — it produces a clean, linear history. Use merge to integrate completed feature branches into main, which preserves the context of when the feature was developed. Never rebase commits that have been pushed to a shared branch, as this rewrites history that other developers depend on.
How often should I commit?
Commit whenever you reach a logical checkpoint — a working function, a passing test, a completed refactoring step. Small, focused commits are easier to review, easier to revert, and produce a more useful history than large commits that bundle unrelated changes. A good rule of thumb: if you cannot summarize your changes in a single sentence, the commit is too large.