Git Version Control Essentials
Essential Git commands and workflows for modern software development.
Essential Git commands and workflows for modern software development.

Introduction: Imagine working on a critical project and accidentally deleting hours of work, or trying to collaborate with a team where everyone's changes constantly overwrite each other's code. These scenarios are nightmares that version control systems were designed to prevent. Git has become the industry standard for managing code changes, enabling millions of developers worldwide to collaborate effectively and maintain a complete history of their projects.
Git is a free and open-source distributed version control system designed to handle everything from small to very large projects with speed and efficiency. Created by Linus Torvalds in 2005, Git has revolutionized how developers work together, making it possible to experiment freely, collaborate seamlessly, and maintain a reliable record of every change made to a codebase.
Before diving into Git commands and workflows, it's important to understand why version control is essential in modern software development.
Without version control, developers face numerous challenges:
Git addresses these challenges by providing:
Complete History: Every change is recorded with who made it, when, and why. You can review the entire evolution of your project and understand the reasoning behind decisions made months or years ago.
Safe Experimentation: Create branches to try new features without affecting the main codebase. If the experiment fails, simply delete the branch. If it succeeds, merge it back in.
Collaboration Support: Multiple developers can work on different features simultaneously without conflicts. Git intelligently merges changes and alerts you when manual intervention is needed.
Backup and Recovery: With Git, your code exists in multiple locations. Even if your local machine fails, the code is safe in remote repositories. You can recover any previous version of any file at any time.
Code Review and Quality: Git enables code review processes where team members can examine changes before they're merged into the main codebase, improving code quality and knowledge sharing.
Before working with Git commands, let's understand the core concepts that make Git powerful.
Unlike older version control systems (like SVN), Git is distributed. This means:
Git has three main states that your files can be in:
Modified: You've changed the file but haven't committed it to your database yet.
# You edit a file - it's now modified
vim index.js
Staged: You've marked a modified file in its current version to go into your next commit.
# Stage the file - it's now staged
git add index.js
Committed: The data is safely stored in your local database.
# Commit the file - it's now committed
git commit -m "Update homepage functionality"
Correspondingly, Git has three main sections:
Working Directory: The files you see and edit in your project folder. This is a single checkout of one version of the project, extracted from the compressed database in the Git directory.
Staging Area (Index): A file that stores information about what will go into your next commit. Think of it as a preview of your next commit.
Git Directory (Repository): Where Git stores the metadata and object database for your project. This is the most important part of Git, and it's what's copied when you clone a repository.
A commit is a snapshot of your project at a specific point in time. Each commit:
Think of commits as save points in a video game. You can always return to any save point to see what your project looked like at that moment.
Before you can start using Git, you need to install and configure it properly.
macOS:
# Using Homebrew
brew install git
# Or download from git-scm.com
Linux:
# Ubuntu/Debian
sudo apt-get install git
# Fedora
sudo dnf install git
Windows:
Download Git for Windows from git-scm.com, which includes Git Bash, a terminal emulator.
After installation, configure your identity. This information will be included in every commit you make.
# Set your name
git config --global user.name "Your Name"
# Set your email
git config --global user.email "your.email@example.com"
# Set default branch name
git config --global init.defaultBranch main
# Set default editor
git config --global core.editor "code --wait"
# View all settings
git config --list
# View a specific setting
git config user.name
Setting up SSH keys allows you to connect to remote repositories without entering your password every time.
# Generate SSH key
ssh-keygen -t ed25519 -C "your.email@example.com"
# Start SSH agent
eval "$(ssh-agent -s)"
# Add your key to the agent
ssh-add ~/.ssh/id_ed25519
# Copy public key to clipboard (macOS)
pbcopy < ~/.ssh/id_ed25519.pub
Then add the public key to your GitHub/GitLab/Bitbucket account settings.
The basic Git workflow follows a simple pattern that you'll use hundreds of times. Let's walk through each step in detail.
Starting a new project:
# Create a new directory
mkdir my-project
cd my-project
# Initialize Git repository
git init
This creates a .git directory that contains all the repository metadata.
Working on an existing project:
# Clone a repository
git clone https://github.com/username/repository.git
# Clone with a different folder name
git clone https://github.com/username/repository.git my-folder
Before making changes, it's good practice to check the current state of your repository.
git status
This shows:
Edit your files using your preferred editor. Git will detect these changes.
# Create a new file
echo "# My Project" > README.md
# Edit an existing file
vim index.js
Staging allows you to selectively choose which changes to include in your next commit.
# Stage a specific file
git add README.md
# Stage all changes in current directory
git add .
# Stage all changes in the repository
git add -A
# Stage specific files with pattern
git add *.js
# Stage parts of a file interactively
git add -p
Why staging? Staging gives you control over what goes into each commit, allowing you to create logical, focused commits even if you've made changes to multiple files.
A commit saves the staged changes to the repository history.
# Commit with a message
git commit -m "Add README and update homepage"
# Commit with detailed message (opens editor)
git commit
# Stage and commit modified tracked files in one step
git commit -am "Quick update to existing files"
Writing good commit messages:
# Good commit messages
git commit -m "Add user authentication feature"
git commit -m "Fix navigation menu on mobile devices"
git commit -m "Update dependencies to latest versions"
# Bad commit messages
git commit -m "Fixed stuff"
git commit -m "Changes"
git commit -m "asdf"
# View commit history
git log
# Compact one-line format
git log --oneline
# Show graph of branches
git log --oneline --graph --all
# Show last 5 commits
git log -5
# Show commits by specific author
git log --author="John"
After committing locally, push your changes to a remote repository.
# Push to remote repository
git push origin main
# Push and set upstream
git push -u origin main
# Push all branches
git push --all
Branches are one of Git's most powerful features, allowing you to diverge from the main line of development and work independently.
Think of branches as parallel universes for your code. You can create a branch, make changes, and then merge those changes back or discard them entirely.
Feature Development: Develop new features without affecting the stable main branch.
Bug Fixes: Fix bugs in isolation and test thoroughly before merging.
Experimentation: Try radical changes without risk.
Collaboration: Multiple developers can work on different features simultaneously.
Create a new branch:
# Create a new branch
git branch feature-login
# Create and switch to new branch
git checkout -b feature-login
# Newer syntax (Git 2.23+)
git switch -c feature-login
List branches:
# List local branches
git branch
# List all branches (including remote)
git branch -a
# List with last commit info
git branch -v
Switch branches:
# Switch to existing branch
git checkout main
# Newer syntax
git switch main
Delete branches:
# Delete merged branch
git branch -d feature-login
# Force delete unmerged branch
git branch -D feature-login
# Delete remote branch
git push origin --delete feature-login
Good branch names are descriptive and follow a consistent pattern:
# Feature branches
git checkout -b feature/user-authentication
git checkout -b feature/payment-integration
# Bug fix branches
git checkout -b fix/navbar-mobile-layout
git checkout -b bugfix/memory-leak
# Hotfix branches
git checkout -b hotfix/security-vulnerability
# Release branches
git checkout -b release/v1.2.0
# Show differences between branches
git diff main feature-login
# Show files that differ
git diff --name-only main feature-login
# Show commit differences
git log main..feature-login
Two primary ways to integrate changes from one branch into another are merging and rebasing. Understanding when to use each is crucial.
Merging creates a new "merge commit" that ties together the histories of both branches.
How to merge:
# Switch to the branch you want to merge into
git checkout main
# Merge the feature branch
git merge feature-login
What happens:
Advantages:
Disadvantages:
When to use:
Rebasing moves the entire feature branch to begin on the tip of another branch, creating a linear history.
How to rebase:
# While on feature branch
git checkout feature-login
git rebase main
# Or in one command
git rebase main feature-login
What happens:
Advantages:
Disadvantages:
When to use:
Interactive rebase is powerful for cleaning up commit history before sharing.
# Rebase last 5 commits interactively
git rebase -i HEAD~5
This allows you to:
Example:
pick a1b2c3d Add login form
squash d4e5f6g Fix typo in login
pick g7h8i9j Add logout functionality
reword j0k1l2m Update user profile
Never rebase public branches. Once you've pushed commits to a shared repository and others might have based work on them, don't rebase. Use merge instead.
Remote repositories enable collaboration with other developers and serve as backups for your code.
A remote is a version of your repository hosted on the internet or network. You can have multiple remotes.
View remotes:
# List remotes
git remote
# List remotes with URLs
git remote -v
# Show detailed remote info
git remote show origin
# Add a remote
git remote add origin https://github.com/username/repo.git
# Add additional remote
git remote add upstream https://github.com/original/repo.git
Fetch: Downloads changes from remote but doesn't merge them.
# Fetch from origin
git fetch origin
# Fetch from all remotes
git fetch --all
# Fetch and prune deleted branches
git fetch -p
Pull: Fetches and merges changes in one step.
# Pull from current branch's upstream
git pull
# Pull with rebase instead of merge
git pull --rebase
# Pull from specific remote and branch
git pull origin main
# Push to origin remote, main branch
git push origin main
# Push and set upstream tracking
git push -u origin feature-branch
# Push all branches
git push --all
# Push tags
git push --tags
# Force push (dangerous!)
git push --force
# Safer force push
git push --force-with-lease
Tracking branches are local branches that have a direct relationship to a remote branch.
# Create tracking branch
git checkout -b feature origin/feature
# Set upstream for existing branch
git branch --set-upstream-to=origin/main main
# View tracking relationships
git branch -vv
Here's a comprehensive reference of essential Git commands you'll use regularly.
# Check repository status
git status
# Short status
git status -s
# Show commit history
git log
# Show graph of branches
git log --graph --oneline --all
# Show who changed what in a file
git blame filename.js
# Show changes in a commit
git show commit-hash
# Stage files
git add file.js
git add .
git add -A
# Unstage files
git reset HEAD file.js
# Commit changes
git commit -m "message"
git commit --amend
# Remove files
git rm file.js
# Move/rename files
git mv old.js new.js
# Show unstaged changes
git diff
# Show staged changes
git diff --staged
# Show changes between commits
git diff commit1 commit2
# Show changes in a file
git diff filename.js
# Create branch
git branch branch-name
# Switch branch
git checkout branch-name
git switch branch-name
# Create and switch
git checkout -b branch-name
# Delete branch
git branch -d branch-name
# Merge branch
git merge branch-name
# Clone repository
git clone url
# Fetch changes
git fetch origin
# Pull changes
git pull
# Push changes
git push origin branch-name
# View remotes
git remote -v
Save changes temporarily without committing.
# Stash changes
git stash
# Stash with message
git stash save "work in progress"
# List stashes
git stash list
# Apply last stash
git stash apply
# Apply and remove stash
git stash pop
# Apply specific stash
git stash apply stash@{0}
# Drop stash
git stash drop stash@{0}
# Clear all stashes
git stash clear
# 1. Update main branch
git checkout main
git pull origin main
# 2. Create feature branch
git checkout -b feature/new-feature
# 3. Make changes and commit
git add .
git commit -m "Implement new feature"
# 4. Push feature branch
git push -u origin feature/new-feature
# 5. Create pull request on GitHub/GitLab
# (Done through web interface)
# 6. After review and approval, merge on platform
# or merge locally:
git checkout main
git pull origin main
git merge feature/new-feature
# 7. Delete feature branch
git branch -d feature/new-feature
git push origin --delete feature/new-feature
A more structured workflow for release management.
Main branches:
main: Production-ready codedevelop: Integration branch for featuresSupporting branches:
feature/*: New featuresrelease/*: Release preparationhotfix/*: Emergency production fixes# Start new feature
git checkout develop
git checkout -b feature/new-feature
# Finish feature
git checkout develop
git merge feature/new-feature
git branch -d feature/new-feature
# Start release
git checkout develop
git checkout -b release/1.0.0
# Finish release
git checkout main
git merge release/1.0.0
git tag -a v1.0.0
git checkout develop
git merge release/1.0.0
git branch -d release/1.0.0
Common in open-source projects.
# 1. Fork repository on GitHub
# 2. Clone your fork
git clone https://github.com/yourusername/repo.git
# 3. Add upstream remote
git remote add upstream https://github.com/original/repo.git
# 4. Create feature branch
git checkout -b feature/contribution
# 5. Make changes and commit
git add .
git commit -m "Add contribution"
# 6. Push to your fork
git push origin feature/contribution
# 7. Create pull request on GitHub
# 8. Keep your fork updated
git fetch upstream
git checkout main
git merge upstream/main
git push origin main
Mistakes happen. Here's how to undo them safely.
# Unstage specific file
git reset HEAD file.js
# Unstage all files
git reset HEAD
# Discard changes to a file
git checkout -- file.js
# Discard all changes
git checkout -- .
# Newer syntax
git restore file.js
# Change commit message
git commit --amend -m "New message"
# Add forgotten files to last commit
git add forgotten.js
git commit --amend --no-edit
# Undo last commit, keep changes staged
git reset --soft HEAD~1
# Undo last commit, keep changes unstaged
git reset HEAD~1
# Undo last commit, discard changes
git reset --hard HEAD~1
# Undo multiple commits
git reset --hard HEAD~3
Creates new commits that undo previous commits (safer for shared branches).
# Revert last commit
git revert HEAD
# Revert specific commit
git revert commit-hash
# Revert without committing immediately
git revert -n commit-hash
# View reflog
git reflog
# Recover lost commit
git checkout commit-hash
# Create branch from lost commit
git checkout -b recovered-branch commit-hash
Commit often: Small, frequent commits are better than large, infrequent ones.
Write meaningful messages: Your future self will thank you.
# Good
git commit -m "Add password validation to login form"
# Bad
git commit -m "stuff"
Commit complete, logical changes: Each commit should represent one logical change.
Use descriptive branch names: feature/user-authentication not fix-1
Keep branches short-lived: Merge or delete branches regularly
Don't commit to main directly: Always use feature branches
Pull before you push: Always fetch and merge before pushing
Review before merging: Use pull requests for code review
Communicate: Let teammates know about major changes
Never commit sensitive data: Passwords, API keys, secrets
Use .gitignore: Exclude files that shouldn't be tracked
# .gitignore example
node_modules/
.env
*.log
.DS_Store
Review changes before committing: Use git diff to check what you're committing
When Git can't automatically merge changes:
# 1. Git will mark conflict in file
<<<<<<< HEAD
your changes
=======
their changes
>>>>>>> branch-name
# 2. Edit the file to resolve
# 3. Stage resolved file
git add conflicted-file.js
# 4. Complete merge
git commit
# 1. Copy commit hash
git log --oneline
# 2. Switch to correct branch
git checkout correct-branch
# 3. Cherry-pick the commit
git cherry-pick commit-hash
# 4. Go back and remove from wrong branch
git checkout wrong-branch
git reset --hard HEAD~1
# 1. Remove file
git rm --cached sensitive-file
# 2. Add to .gitignore
echo "sensitive-file" >> .gitignore
# 3. Commit removal
git commit -m "Remove sensitive file"
# 4. Force push (if repository is not shared)
git push --force
# 5. Consider repository as compromised
# Rotate any exposed credentials immediately
# Change author of last commit
git commit --amend --author="Name <email@example.com>"
# Change author of multiple commits (interactive rebase)
git rebase -i HEAD~5
# Mark commits as 'edit', then for each:
git commit --amend --author="Name <email@example.com>"
git rebase --continue
Git is an incredibly powerful tool that becomes more intuitive with practice. While the learning curve can feel steep initially, mastering Git fundamentals will make you a more effective developer and enable seamless collaboration with teams worldwide.
The key is to start simple. Use the basic workflow daily, experiment with branches, and gradually incorporate more advanced features as you become comfortable. Don't be afraid to make mistakes in your local repository—that's what Git is designed to handle.
Remember, every expert Git user was once a beginner. The difference is practice and persistence. Keep committing, keep learning, and Git will become second nature.