Why Git Matters
Git is the version control system behind virtually every modern software project. Whether you're solo or on a 200-person team, how you use Git directly impacts code quality, release velocity, and team sanity.
This guide covers Git fundamentals, the three dominant branching strategies, merge vs rebase tradeoffs, conflict resolution, and commit message conventions that actually help.
Git Fundamentals
The Three Areas
Every Git repository has three conceptual areas that files move between:
+-------------------+ git add +-------------------+ git commit +-------------------+
| Working Directory | -------------> | Staging Area | -------------> | Repository |
| (your files) | <------------- | (index) | | (.git history) |
+-------------------+ git restore +-------------------+ +-------------------+
- Working Directory -- the actual files on disk you edit
- Staging Area (Index) -- a snapshot of what will go into the next commit
- Repository -- the complete history of committed snapshots
Essential Commands
# Initialize a new repository
git init
# Clone an existing repository
git clone https://github.com/user/repo.git
# Check current status
git status
# Stage specific files
git add src/app.ts src/utils.ts
# Stage all changes
git add .
# Commit staged changes
git commit -m "feat: add user authentication"
# Push to remote
git push origin main
# Pull latest changes
git pull origin main
# Create and switch to a new branch
git checkout -b feature/login-page
# Switch to an existing branch
git checkout main
# Modern alternative to checkout for switching
git switch main
git switch -c feature/login-page
Viewing History
# Full commit log
git log
# Compact one-line log
git log --oneline
# Visual branch graph
git log --oneline --graph --all
# Show changes in last commit
git show
# Show differences between working directory and staging
git diff
# Show differences between staging and last commit
git diff --staged
Undoing Changes
# Unstage a file (keep changes in working directory)
git restore --staged src/app.ts
# Discard working directory changes for a file
git restore src/app.ts
# Amend the last commit message (before pushing)
git commit --amend -m "fix: correct typo in auth module"
# Revert a specific commit (creates a new commit that undoes it)
git revert abc1234
# Reset to a previous commit (destructive -- use with caution)
git reset --hard HEAD~1
Branching Strategies
Choosing the right branching strategy depends on team size, release cadence, and project maturity. Here are the three most widely used.
Strategy 1: GitHub Flow
GitHub Flow is the simplest strategy. It works well for web apps with continuous deployment.
main ----*----*----*----*----*----*----*----*---->
\ /
feature/login *----*----*------
(PR + review)
Rules:
mainis always deployable- Create a feature branch from
main - Make commits on the feature branch
- Open a Pull Request for review
- Merge to
mainafter approval - Deploy from
main
# Start feature
git checkout main
git pull origin main
git checkout -b feature/user-dashboard
# Work on feature
git add .
git commit -m "feat: add dashboard layout"
git commit -m "feat: add activity feed component"
# Push and open PR
git push -u origin feature/user-dashboard
# Open PR on GitHub, get review, merge
Best for: small teams, SaaS products, continuous deployment.
Drawbacks: no concept of releases or hotfixes as first-class citizens.
Strategy 2: Git Flow
Git Flow is a more structured strategy with dedicated branches for releases and hotfixes. Created by Vincent Driessen in 2010.
main ----*-----------------*-----------*---->
\ / \ /
release \ *---*--- \ /
\ / \ /
develop ----*---*---*---*---*----*---*---*---->
\ / \ /
feature *---* *---*-
Branches:
main-- production-ready code, tagged with version numbersdevelop-- integration branch for featuresfeature/*-- individual features, branch fromdeveloprelease/*-- release preparation, branch fromdevelophotfix/*-- urgent production fixes, branch frommain
# Start a feature
git checkout develop
git checkout -b feature/payment-system
# Finish feature -- merge back to develop
git checkout develop
git merge --no-ff feature/payment-system
# Start a release
git checkout develop
git checkout -b release/2.1.0
# Finish release -- merge to main AND develop
git checkout main
git merge --no-ff release/2.1.0
git tag -a v2.1.0 -m "Release 2.1.0"
git checkout develop
git merge --no-ff release/2.1.0
# Hotfix
git checkout main
git checkout -b hotfix/critical-auth-bug
# fix the bug...
git checkout main
git merge --no-ff hotfix/critical-auth-bug
git tag -a v2.1.1 -m "Hotfix 2.1.1"
git checkout develop
git merge --no-ff hotfix/critical-auth-bug
Best for: large teams, software with formal release cycles, mobile apps, libraries.
Drawbacks: complex, many long-lived branches, merge conflicts increase.
Strategy 3: Trunk-Based Development
Trunk-based development keeps all work on a single branch (main or trunk) with very short-lived feature branches (hours, not days).
main ----*--*--*--*--*--*--*--*--*--*--*--*---->
\ / \ / \ /
** ** *
(short-lived feature branches)
Rules:
- Everyone commits to
main(or merges within hours) - Feature branches live at most 1-2 days
- Use feature flags to hide incomplete work
- CI runs on every commit
- Broken builds are fixed immediately
# Short-lived branch
git checkout -b feat/button-color
git add .
git commit -m "feat: update button color to brand blue"
git push -u origin feat/button-color
# PR, review, merge same day
# Or commit directly to main (with feature flag)
git checkout main
git add .
git commit -m "feat: add search (behind flag ENABLE_SEARCH)"
git push origin main
Best for: experienced teams, high deployment frequency, microservices.
Drawbacks: requires strong CI/CD, feature flags add complexity.
Comparison Table
| Aspect | GitHub Flow | Git Flow | Trunk-Based |
|---|---|---|---|
| Complexity | Low | High | Low |
| Branch Lifespan | Days | Days to weeks | Hours |
| Release Process | Deploy main | Dedicated release branch | Deploy main |
| Hotfix Process | Branch from main | Dedicated hotfix branch | Commit to main |
| Best Team Size | 1-15 | 10-100+ | 5-50 |
| Deploy Frequency | Continuous | Scheduled | Continuous |
| CI/CD Requirement | Medium | Low | High |
| Learning Curve | Easy | Steep | Easy (hard discipline) |
Merge vs Rebase
This is the most debated topic in Git. Both achieve the same result (integrating changes) but produce different histories.
Merge
Merge creates a new "merge commit" that combines two branches. It preserves the full history.
git checkout main
git merge feature/login
Before merge:
main ----A----B----C
\
feature D----E
After merge:
main ----A----B----C---------F (merge commit)
\ /
feature D----E
Rebase
Rebase replays your commits on top of another branch. It creates a linear history.
git checkout feature/login
git rebase main
Before rebase:
main ----A----B----C
\
feature D----E
After rebase:
main ----A----B----C
\
feature D'----E' (replayed commits)
Merge vs Rebase Comparison
| Aspect | Merge | Rebase |
|---|---|---|
| History | Non-linear (branched) | Linear (straight line) |
| Merge Commits | Yes (extra commits) | No |
| Commit Hashes | Preserved | Changed (rewritten) |
| Shared Branches | Safe | Dangerous (rewrites history) |
| Conflict Resolution | Once (at merge) | Per commit (during replay) |
| Traceability | Full branch context | Cleaner but less context |
| Undo Difficulty | Easy (revert merge commit) | Harder (history rewritten) |
The Golden Rule of Rebase
Never rebase commits that have been pushed to a shared branch. Rebasing rewrites commit hashes. If others have based work on those commits, you will cause conflicts for the entire team.
# Safe: rebase your local feature branch onto updated main
git checkout feature/my-work
git rebase main
# Dangerous: NEVER do this
git checkout main
git rebase feature/my-work
Squash Merge
A popular middle ground. Squash all feature commits into a single commit on main.
# On GitHub: "Squash and merge" button
# Or manually:
git checkout main
git merge --squash feature/login
git commit -m "feat: add login page with OAuth support"
Before:
feature D----E----F----G (4 messy commits)
After squash merge:
main ----A----B----C----H (single clean commit)
Merge Conflict Resolution
Conflicts happen when two branches change the same lines. Git marks the conflicting sections.
What a Conflict Looks Like
function getGreeting(name) {
<<<<<<< HEAD
return `Hello, ${name}! Welcome back.`;
=======
return `Hey ${name}, good to see you!`;
>>>>>>> feature/new-greeting
}
<<<<<<< HEAD-- your current branch's version=======-- separator>>>>>>> feature/new-greeting-- the incoming branch's version
Resolution Steps
# 1. Attempt the merge
git merge feature/new-greeting
# CONFLICT: Merge conflict in src/greeting.ts
# 2. Open conflicted files and resolve manually
# Pick one version, combine both, or write something new
# 3. After editing, the resolved file looks like:
function getGreeting(name) {
return `Hey ${name}! Welcome back.`;
}
# 4. Stage the resolved file
git add src/greeting.ts
# 5. Complete the merge
git commit -m "merge: resolve greeting conflict"
Preventing Conflicts
- Pull frequently -- stay up to date with main
- Keep branches short-lived -- less time = less divergence
- Communicate -- if two people work on the same file, coordinate
- Use consistent formatting -- auto-formatters prevent whitespace conflicts
# Stay up to date while working on a feature
git checkout feature/dashboard
git fetch origin
git rebase origin/main
# Resolve any small conflicts incrementally
Aborting a Merge
If a conflict is too complex and you want to start over:
git merge --abort
This returns your branch to the state before the merge attempt.
Commit Message Best Practices
Good commit messages make projects maintainable. Bad ones are useless noise.
Conventional Commits
The Conventional Commits specification provides a structured format:
<type>(<scope>): <description>
[optional body]
[optional footer(s)]
Types:
feat-- new featurefix-- bug fixdocs-- documentation onlystyle-- formatting, no code changerefactor-- code change that neither fixes a bug nor adds a featuretest-- adding or correcting testschore-- build process, tooling, dependencies
Examples
# Feature
git commit -m "feat(auth): add Google OAuth login"
# Bug fix
git commit -m "fix(cart): prevent negative quantity on item update"
# Breaking change
git commit -m "feat(api)!: change response format to JSON:API spec
BREAKING CHANGE: All API responses now follow JSON:API format.
Clients must update their parsers."
# With scope and body
git commit -m "refactor(database): migrate from callbacks to async/await
Replaced all callback-based database queries with async/await.
No functional changes. All existing tests pass."
The Seven Rules of Great Commit Messages
- Separate subject from body with a blank line
- Limit the subject line to 50 characters
- Capitalize the subject line
- Do not end the subject line with a period
- Use the imperative mood ("add feature" not "added feature")
- Wrap the body at 72 characters
- Explain the what and why, not the how
Bad vs Good Messages
# Bad
git commit -m "fix stuff"
git commit -m "WIP"
git commit -m "asdfasdf"
git commit -m "changes"
# Good
git commit -m "fix(auth): handle expired token refresh race condition"
git commit -m "feat(search): add fuzzy matching for product names"
git commit -m "docs: add API rate limiting section to README"
git commit -m "test(cart): add edge case for empty cart checkout"
Git Tags
Tags mark specific points in history, typically used for releases.
# Create annotated tag (recommended for releases)
git tag -a v1.0.0 -m "Initial stable release"
# Create lightweight tag
git tag v1.0.0-beta
# List tags
git tag -l
# List tags matching a pattern
git tag -l "v1.*"
# Push a specific tag
git push origin v1.0.0
# Push all tags
git push origin --tags
# Delete a local tag
git tag -d v1.0.0-beta
# Delete a remote tag
git push origin --delete v1.0.0-beta
Semantic Versioning with Tags
Follow semver (MAJOR.MINOR.PATCH):
v1.0.0 # Initial release
v1.1.0 # New feature (backward compatible)
v1.1.1 # Bug fix
v2.0.0 # Breaking change
Git Stash
Stash lets you save uncommitted changes without committing them. Useful when you need to switch branches quickly.
# Stash current changes
git stash
# Stash with a descriptive message
git stash push -m "WIP: dashboard layout changes"
# List stashes
git stash list
# Apply most recent stash (keep it in stash list)
git stash apply
# Apply and remove most recent stash
git stash pop
# Apply a specific stash
git stash apply stash@{2}
# Drop a specific stash
git stash drop stash@{0}
# Clear all stashes
git stash clear
.gitignore Best Practices
A .gitignore file tells Git which files to exclude from tracking.
# Dependencies
node_modules/
vendor/
# Build output
dist/
build/
.next/
out/
# Environment files (secrets!)
.env
.env.local
.env.production
# IDE files
.vscode/settings.json
.idea/
# OS files
.DS_Store
Thumbs.db
# Logs
*.log
npm-debug.log*
# Test coverage
coverage/
# Temporary files
*.tmp
*.swp
# If you already committed a file and want to stop tracking it:
git rm --cached .env
echo ".env" >> .gitignore
git commit -m "chore: remove .env from tracking"
Git Aliases for Productivity
Add these to your ~/.gitconfig to speed up daily work:
[alias]
s = status
co = checkout
br = branch
ci = commit
lg = log --oneline --graph --all --decorate
last = log -1 HEAD
unstage = restore --staged
undo = reset HEAD~1 --mixed
amend = commit --amend --no-edit
branches = branch -a
stashes = stash list
wip = !git add . && git commit -m "WIP"
# Usage
git s # instead of git status
git co main # instead of git checkout main
git lg # pretty log graph
git undo # undo last commit, keep changes
Common Workflows Cheat Sheet
Starting a New Feature
git checkout main
git pull origin main
git checkout -b feature/TICKET-123-add-search
# work...
git add .
git commit -m "feat(search): add basic search endpoint"
git push -u origin feature/TICKET-123-add-search
# open PR
Updating a Feature Branch
git checkout feature/TICKET-123-add-search
git fetch origin
git rebase origin/main
# resolve conflicts if any
git push --force-with-lease # safe force push for rebased branch
Emergency Hotfix
git checkout main
git pull origin main
git checkout -b hotfix/fix-payment-crash
# fix the bug...
git add .
git commit -m "fix(payment): handle null card token"
git push -u origin hotfix/fix-payment-crash
# open PR, get emergency review, merge
Cleaning Up Merged Branches
# Delete local branch
git branch -d feature/TICKET-123-add-search
# Delete remote branch
git push origin --delete feature/TICKET-123-add-search
# Prune remote tracking branches that no longer exist
git fetch --prune
Key Takeaways
- Git Flow for structured releases, GitHub Flow for simplicity, Trunk-Based for speed
- Merge preserves history, rebase linearizes it -- never rebase shared branches
- Squash merge is a practical middle ground for clean main history
- Conventional commits make changelogs and automation possible
- Pull frequently, keep branches short-lived, communicate with your team
- Stash saves you when you need to context-switch without committing
- Good
.gitignoreprevents secrets and junk from entering the repo