Git is the version control system used by virtually every software team on earth. It tracks every change you make to code, lets you experiment safely in branches, collaborate without overwriting each other's work, and roll back mistakes in seconds.
This tutorial takes you from zero — no Git installed — to confidently using Git in real projects.
What you'll learn
| Topic | What you'll be able to do |
|---|---|
| Setup | Install Git, configure your identity |
| Core concepts | Understand repos, commits, staging |
| Basic workflow | init → add → commit → log |
| Branches | Create, switch, merge, delete branches |
| Remote repos | clone, push, pull, fetch |
| Undoing changes | reset, revert, restore, stash |
| Real workflows | Feature branch, GitFlow basics |
| Collaboration | Merge conflicts, pull requests |
Why Git?
Before Git, teams emailed zip files or used shared folders. One overwrite and a week of work was gone.
| Without Git | With Git |
|---|---|
| "final_final_v3.zip" | Every version saved forever |
| One person edits at a time | Teams work simultaneously |
| Mistakes are permanent | Roll back to any point |
| No record of who changed what | Full blame history |
| Scared to experiment | Branches make experiments safe |
| Merging = manual copy-paste | Git merges automatically |
Git was created by Linus Torvalds in 2005 to manage the Linux kernel. Today it's the universal standard — used by Google, Microsoft, Netflix, NASA, and solo developers alike.
Part 1 — Install & Configure Git
Install Git
Windows: Download from git-scm.com and run the installer. Use all defaults.
macOS:
# Option 1: Xcode Command Line Tools (installs automatically when you run git)
git --version
# Option 2: Homebrew
brew install git
Linux (Debian/Ubuntu):
sudo apt update && sudo apt install git
Linux (Fedora/RHEL):
sudo dnf install git
Verify installation:
git --version
# git version 2.44.0
Configure your identity
Git stamps every commit with your name and email. Set them once:
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
Other useful settings:
# Set VS Code as your default editor
git config --global core.editor "code --wait"
# Set default branch name to 'main' (modern standard)
git config --global init.defaultBranch main
# Better diff output on Windows
git config --global core.autocrlf true
# View your full config
git config --list
Config is stored in ~/.gitconfig:
[user]
name = Your Name
email = you@example.com
[core]
editor = code --wait
autocrlf = true
[init]
defaultBranch = main
Part 2 — Core Concepts
Before typing commands, understand what Git actually tracks.
The three areas
Working Directory Staging Area (Index) Repository (.git/)
----------------- -------------------- ------------------
Your files as Snapshot of what Permanent history
you edit them will be committed of all commits
│ │ │
└──── git add ───────┘ │
└──── git commit ───────┘
| Area | What it is | How to affect it |
|---|---|---|
| Working Directory | Files on disk | Edit files normally |
| Staging Area | What's queued for the next commit | git add |
| Repository | Permanent commit history | git commit |
What is a commit?
A commit is a snapshot of your staged files at a point in time. It contains:
- A unique SHA hash (e.g.,
a1b2c3d) - Author, date, and message
- A pointer to the previous commit (parent)
Commits form a linked chain — the history of your project:
A ──── B ──── C ──── D (main branch)
Initial Add Fix New
commit auth bug feature
What is a repository?
A repository (repo) is a folder that Git tracks. It contains:
- Your project files
- A hidden
.git/folder with all history, branches, and config
my-project/
├── .git/ ← Git's internal database (don't touch)
│ ├── HEAD
│ ├── config
│ ├── objects/ ← all commits, files, trees stored here
│ └── refs/ ← branch and tag pointers
├── index.html
├── style.css
└── app.js
Part 3 — Your First Repository
Create a new repo
mkdir my-project
cd my-project
git init
Output:
Initialized empty Git repository in /home/user/my-project/.git/
Check status
git status is your most-used command. Run it constantly:
git status
# On branch main
# No commits yet
# nothing to commit
Create a file and make your first commit
echo "# My Project" > README.md
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
Stage the file:
git add README.md
git status
On branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: README.md
Commit it:
git commit -m "Initial commit: add README"
[main (root-commit) a1b2c3d] Initial commit: add README
1 file changed, 1 insertion(+)
create mode 100644 README.md
View history
git log
commit a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9 (HEAD -> main)
Author: Your Name <you@example.com>
Date: Wed Jul 16 10:00:00 2026 +0000
Initial commit: add README
Shorter view:
git log --oneline
# a1b2c3d Initial commit: add README
Pretty graph:
git log --oneline --graph --all
Part 4 — The Daily Workflow
Staging commands
# Stage a single file
git add filename.txt
# Stage multiple files
git add file1.txt file2.txt
# Stage all changes in current directory
git add .
# Stage parts of a file interactively
git add -p filename.txt
# Unstage a file (keep changes in working directory)
git restore --staged filename.txt
Checking differences
# Changes not yet staged
git diff
# Changes staged (ready to commit)
git diff --staged
# Changes between two commits
git diff a1b2c3d e4f5g6h
# Changes in a single file
git diff HEAD filename.txt
Making good commits
A commit should represent one logical change. Bad vs good:
# BAD: everything in one commit
git add .
git commit -m "stuff"
# GOOD: separate logical commits
git add auth.js
git commit -m "feat: add JWT authentication middleware"
git add user.test.js
git commit -m "test: add unit tests for auth middleware"
Commit message format (Conventional Commits):
<type>: <short description>
[optional body]
[optional footer]
| Type | When to use |
|---|---|
feat |
New feature |
fix |
Bug fix |
docs |
Documentation only |
style |
Formatting, no logic change |
refactor |
Code restructure, no feature/fix |
test |
Adding or fixing tests |
chore |
Build process, dependencies |
perf |
Performance improvement |
Examples:
feat: add user registration endpoint
fix: prevent crash when email is null
docs: update API authentication guide
chore: upgrade Jest to v29
Amending commits
Fix the last commit (before pushing):
# Add a forgotten file to the last commit
git add forgotten-file.txt
git commit --amend --no-edit
# Change the last commit message
git commit --amend -m "feat: better commit message"
Never amend commits you've already pushed — it rewrites history and breaks collaborators.
Part 5 — Branches
Branches let you work on features, fixes, or experiments without touching the main codebase.
How branches work
main: A ──── B ──── C
│
feature: └──── D ──── E
main stays stable. You work on feature. When done, merge feature back into main.
Branch commands
# List branches (* = current)
git branch
# * main
# feature/login
# Create a new branch
git branch feature/user-auth
# Switch to a branch
git switch feature/user-auth
# (older syntax: git checkout feature/user-auth)
# Create and switch in one command
git switch -c feature/user-auth
# Delete a branch (after merging)
git branch -d feature/user-auth
# Force delete (even if not merged)
git branch -D feature/user-auth
# Rename current branch
git branch -m new-name
# List all branches including remote
git branch -a
Working on a branch
# 1. Start from main
git switch main
git pull # get latest changes
# 2. Create feature branch
git switch -c feature/add-login
# 3. Work on the feature
echo "login code" > login.js
git add login.js
git commit -m "feat: add login page"
# 4. More commits
echo "tests" > login.test.js
git add login.test.js
git commit -m "test: add login tests"
# 5. Merge into main when done
git switch main
git merge feature/add-login
# 6. Delete the branch
git branch -d feature/add-login
Merge types
Fast-forward merge (main didn't change while you worked):
Before:
main: A ──── B
└──── C ──── D (feature)
After git merge feature:
main: A ──── B ──── C ──── D
Git just moves main forward. No merge commit.
Three-way merge (main changed while you worked):
Before:
main: A ──── B ──── E
└──── C ──── D (feature)
After git merge feature:
main: A ──── B ──── E ──── M (merge commit)
└──── C ──── D ─┘
Git creates a merge commit M combining both histories.
# Create a merge commit even when fast-forward is possible
git merge --no-ff feature/login
# Abort a merge in progress
git merge --abort
Part 6 — Merge Conflicts
A merge conflict happens when two branches changed the same lines differently. Git can't decide which version to keep — it asks you.
What a conflict looks like
<<<<<<< HEAD
This is the main branch version.
=======
This is the feature branch version.
>>>>>>> feature/login
<<<<<<< HEAD— your current branch's version=======— divider>>>>>>> feature/branch— the incoming branch's version
Resolving conflicts
# 1. See which files conflict
git status
# both modified: login.js
# 2. Open the file and edit it to the correct version
# (remove the conflict markers, keep what you want)
# 3. After editing, mark as resolved
git add login.js
# 4. Complete the merge
git commit
Using a merge tool
# Open a visual merge tool
git mergetool
# Configure VS Code as merge tool
git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'
Conflict prevention tips
- Pull from main frequently (merge conflicts grow with time)
- Keep branches short-lived (days, not weeks)
- Communicate with teammates which files you're editing
- Break large features into smaller PRs
Part 7 — Remote Repositories
A remote is a version of your repo stored somewhere else — usually GitHub, GitLab, or Bitbucket.
Key remote commands
# View remotes
git remote -v
# origin https://github.com/you/project.git (fetch)
# origin https://github.com/you/project.git (push)
# Add a remote
git remote add origin https://github.com/you/project.git
# Remove a remote
git remote remove origin
# Rename a remote
git remote rename origin upstream
Clone an existing repo
git clone https://github.com/username/repo.git
# Creates a 'repo' folder with the project inside
# Clone into a specific folder name
git clone https://github.com/username/repo.git my-folder
# Clone a specific branch
git clone -b develop https://github.com/username/repo.git
Push and pull
# Push local commits to remote
git push origin main
# Push and set upstream (only needed once per branch)
git push -u origin feature/login
# After this, just: git push
# Pull latest changes (fetch + merge)
git pull origin main
# Fetch without merging
git fetch origin
git merge origin/main # then merge manually
Fetch vs Pull:
| Command | What it does |
|---|---|
git fetch |
Downloads remote changes, doesn't merge |
git pull |
Downloads AND merges (fetch + merge) |
Use git fetch when you want to review changes before merging. Use git pull when you trust the remote and want to sync immediately.
Push rejected?
If someone pushed to the remote after your last pull:
# This fails:
git push origin main
# ! [rejected] main -> main (non-fast-forward)
# Fix: pull first, then push
git pull origin main # merges their changes
git push origin main # now succeeds
Part 8 — Undoing Changes
Git's superpower: nothing is ever truly lost.
Discard working directory changes
# Discard changes to a single file (back to last commit)
git restore filename.txt
# Discard ALL unstaged changes (careful!)
git restore .
Unstage a file
# Remove from staging area, keep working directory changes
git restore --staged filename.txt
Undo commits
# Undo last commit, keep changes staged
git reset --soft HEAD~1
# Undo last commit, keep changes unstaged
git reset --mixed HEAD~1 # (default)
# Undo last commit AND discard all changes (DESTRUCTIVE)
git reset --hard HEAD~1
# Undo last 3 commits, keep changes
git reset --soft HEAD~3
reset vs revert:
| Command | Changes history? | Safe to push? |
|---|---|---|
git reset |
Yes — rewrites history | Only if not pushed yet |
git revert |
No — adds a new "undo" commit | Always safe |
# Create a new commit that undoes a previous commit
git revert a1b2c3d
# Revert last commit without opening an editor
git revert HEAD --no-edit
Use revert on shared branches. Use reset only on private branches.
Stash (save work without committing)
# Save current work to stash
git stash
# Stash with a description
git stash push -m "WIP: login form validation"
# List stashes
git stash list
# stash@{0}: WIP: login form validation
# stash@{1}: On main: WIP: dashboard
# Apply most recent stash (keep it in stash list)
git stash apply
# Apply and remove from stash list
git stash pop
# Apply specific stash
git stash apply stash@{1}
# Delete a stash
git stash drop stash@{0}
# Clear all stashes
git stash clear
When to stash: You're mid-feature when an urgent bug comes in. Stash your work, fix the bug, pop your stash and resume.
Find deleted content
# See all commits including "orphaned" ones
git reflog
# HEAD@{0}: commit: feat: add login
# HEAD@{1}: reset: moving to HEAD~1
# HEAD@{2}: commit: feat: add login ← your "lost" commit
# Recover a lost commit
git checkout HEAD@{2}
# or
git cherry-pick HEAD@{2}
git reflog is your safety net. Git keeps everything for at least 30 days.
Part 9 — Rebase
Rebase is an alternative to merge. Instead of creating a merge commit, it replays your commits on top of another branch.
Before rebase:
main: A ──── B ──── C
└──── D ──── E (feature)
After git rebase main:
main: A ──── B ──── C
└──── D' ──── E' (feature, replayed)
After rebase, feature looks like it was always based on the latest main.
# Rebase feature branch onto main
git switch feature/login
git rebase main
# Interactive rebase: clean up your last 3 commits before merging
git rebase -i HEAD~3
Interactive rebase lets you:
pick— keep the commitsquash/s— combine with previous commitreword/r— change the commit messagedrop/d— delete the commitedit/e— pause and amend
Merge vs Rebase:
| Merge | Rebase | |
|---|---|---|
| History | Preserves full history | Cleaner linear history |
| Merge commits | Creates one | None |
| Conflicts | Resolved once | Resolved per commit |
| Safe on shared branches | Yes | No — rewrites history |
| Best for | Feature merges to main | Cleaning up local history |
Golden rule: Never rebase commits that are on a remote shared branch.
Part 10 — Tags
Tags mark specific commits as important — usually releases.
# List tags
git tag
# Create a lightweight tag
git tag v1.0.0
# Create an annotated tag (recommended — includes message)
git tag -a v1.0.0 -m "Release version 1.0.0"
# Tag a specific commit
git tag -a v0.9.0 a1b2c3d -m "Beta release"
# Push a tag to remote
git push origin v1.0.0
# Push all tags
git push origin --tags
# Delete a local tag
git tag -d v1.0.0
# Delete a remote tag
git push origin --delete v1.0.0
# Check out a specific tag
git checkout v1.0.0
Semantic versioning: v<MAJOR>.<MINOR>.<PATCH>
MAJOR— breaking changesMINOR— new features, backwards compatiblePATCH— bug fixes
Part 11 — .gitignore
.gitignore tells Git which files to never track. Add it to every project.
# Dependencies
node_modules/
vendor/
__pycache__/
*.pyc
# Environment files (never commit secrets!)
.env
.env.local
.env.production
*.env
# Build output
dist/
build/
out/
.next/
# Logs
*.log
logs/
# OS files
.DS_Store
Thumbs.db
desktop.ini
# IDE files
.vscode/settings.json
.idea/
*.swp
*.swo
# Test coverage
coverage/
.nyc_output/
# Database files
*.sqlite
*.db
# If you already committed something that should be ignored:
git rm --cached filename.txt # remove from tracking, keep on disk
git rm --cached -r directory/ # remove a directory
git commit -m "chore: untrack filename.txt"
Check what .gitignore is ignoring:
git status --ignored
git check-ignore -v filename.txt
Part 12 — Real-World Workflows
Feature Branch Workflow (most common)
# 1. Start from an updated main
git switch main
git pull origin main
# 2. Create a feature branch
git switch -c feature/user-profile
# 3. Work and commit often
git add -p # stage selectively
git commit -m "feat: add avatar upload"
git commit -m "feat: add profile bio field"
git commit -m "test: add profile tests"
# 4. Keep up-to-date with main (to minimize conflicts)
git fetch origin
git rebase origin/main
# 5. Push for code review
git push -u origin feature/user-profile
# 6. Open a pull request on GitHub/GitLab
# After approval, merge via the web UI or:
git switch main
git merge --no-ff feature/user-profile
git push origin main
# 7. Clean up
git branch -d feature/user-profile
git push origin --delete feature/user-profile
GitFlow (for versioned releases)
main ──────────────────────────── v1.0 ──── v1.1
develop ──────── feature1 ── feature2 ─────────────────
feature/* / /
hotfix/* ← patch on main
release/* ← prep before tagging
| Branch | Purpose |
|---|---|
main |
Production-ready code only |
develop |
Integration branch for features |
feature/* |
New features from develop |
release/* |
Release prep (version bump, final fixes) |
hotfix/* |
Emergency fixes on main |
Good for: libraries, mobile apps, software with versioned releases.
Trunk-Based Development (for fast teams)
Everyone commits to main (trunk) directly or via short-lived branches (< 1 day). Feature flags hide incomplete work.
# Short-lived branch
git switch -c feat/button-color
# ... one small commit ...
git push origin feat/button-color
# PR reviewed and merged same day
Good for: SaaS products, continuous deployment, experienced teams.
Commit often, push when ready
# Local commits — free, don't cost anything
git commit -m "WIP: starting login form"
git commit -m "WIP: add validation"
git commit -m "WIP: almost done"
# Before pushing, clean up with interactive rebase
git rebase -i origin/main
# squash all WIP commits into one clean commit
# "feat: add login form with validation"
git push origin feature/login
Part 13 — Essential Commands Reference
Navigation & inspection
git status # what's changed
git log --oneline # compact history
git log --oneline --graph --all # visual branch graph
git show a1b2c3d # details of a commit
git diff # unstaged changes
git diff --staged # staged changes
git blame filename.txt # who wrote each line
git shortlog -sn # commits by author
File operations
git add . # stage all
git add -p # stage interactively
git restore file.txt # discard working changes
git restore --staged file.txt # unstage
git rm file.txt # delete and stage removal
git mv old.txt new.txt # rename and stage
Branches
git branch # list local branches
git branch -a # list all including remote
git switch -c feature/name # create and switch
git switch main # switch branch
git merge feature/name # merge into current
git branch -d feature/name # delete merged branch
git branch -D feature/name # force delete
Remote
git clone <url> # clone a repo
git remote -v # list remotes
git fetch origin # download without merging
git pull origin main # download and merge
git push origin main # upload commits
git push -u origin feature # push + set upstream
Undo
git restore file.txt # discard unstaged changes
git restore --staged file.txt # unstage
git commit --amend # fix last commit
git reset --soft HEAD~1 # undo commit, keep staged
git reset --mixed HEAD~1 # undo commit, keep unstaged
git reset --hard HEAD~1 # undo commit, discard changes
git revert HEAD # create undo commit (safe)
git stash # save work temporarily
git stash pop # restore stashed work
Advanced
git rebase main # replay commits on main
git rebase -i HEAD~3 # interactive rebase
git cherry-pick a1b2c3d # copy a commit to current branch
git bisect start # binary search for a bug commit
git tag -a v1.0.0 -m "..." # create annotated tag
git reflog # view all HEAD movements
Part 14 — Three Hands-On Projects
Project 1 — Personal website with version history
mkdir my-website && cd my-website
git init
# Create initial files
cat > index.html << 'EOF'
<!DOCTYPE html>
<html>
<head><title>My Website</title></head>
<body><h1>Hello, World!</h1></body>
</html>
EOF
git add .
git commit -m "feat: initial website"
# Add a style
cat > style.css << 'EOF'
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; }
EOF
# Link it in HTML
sed -i 's|</head>|<link rel="stylesheet" href="style.css"></head>|' index.html
git add .
git commit -m "feat: add CSS styling"
# Experiment in a branch
git switch -c experiment/dark-mode
echo "body { background: #1a1a1a; color: white; }" >> style.css
git add style.css
git commit -m "feat: dark mode experiment"
# View history
git log --oneline --graph --all
# * a1b2c3d (experiment/dark-mode) feat: dark mode experiment
# * b2c3d4e (main) feat: add CSS styling
# * c3d4e5f feat: initial website
# Merge or discard
git switch main
git merge experiment/dark-mode # keep it
# or
# git branch -D experiment/dark-mode # discard it
Project 2 — Collaborate with yourself (two clones)
Simulate a team workflow using two copies:
# Create a "remote" (bare repo)
mkdir ~/repos
git init --bare ~/repos/project.git
# Clone as "Alice"
git clone ~/repos/project.git ~/alice-project
cd ~/alice-project
git config user.name "Alice"
echo "Hello from Alice" > alice.txt
git add alice.txt
git commit -m "feat: alice's file"
git push origin main
# Clone as "Bob"
git clone ~/repos/project.git ~/bob-project
cd ~/bob-project
git config user.name "Bob"
# Bob creates a feature branch
git switch -c feature/bob-work
echo "Hello from Bob" > bob.txt
git add bob.txt
git commit -m "feat: bob's file"
git push -u origin feature/bob-work
# Alice reviews and merges
cd ~/alice-project
git fetch origin
git merge origin/feature/bob-work
git push origin main
Project 3 — Fix a bug with bisect
git bisect uses binary search to find which commit introduced a bug:
# Create a history with a bug introduced midway
git init bug-project && cd bug-project
for i in 1 2 3 4 5; do
echo "version $i" > version.txt
git add .
git commit -m "commit $i"
done
# Bug introduced here
echo "BUG" >> version.txt
git add . && git commit -m "commit 6 (has bug)"
for i in 7 8 9 10; do
echo "version $i" >> version.txt
git add .
git commit -m "commit $i"
done
# Now find when the bug appeared
git bisect start
git bisect bad # current commit is bad
git bisect good HEAD~8 # commit 2 was good
# Git checks out the midpoint commit
# Test it, then tell Git if it's good or bad:
grep -q "BUG" version.txt && git bisect bad || git bisect good
# Repeat until Git finds the first bad commit
# Then reset:
git bisect reset
Common Mistakes
| Mistake | Problem | Fix |
|---|---|---|
Committing directly to main |
Risky in teams, no review | Always work in feature branches |
Committing .env files |
Exposes secrets to the world | Add to .gitignore immediately |
| Vague commit messages | "fix stuff" tells nothing |
Use type: description format |
| Force-pushing to shared branches | Destroys teammates' history | Only force-push to your own branches |
| Huge commits | Hard to review, hard to revert | Commit each logical change separately |
Never running git fetch |
Out-of-date view of remote | git fetch before branching |
Using git reset --hard carelessly |
Data loss | Always check git status first |
| Ignoring merge conflicts | Code breaks silently | Resolve and test immediately |
Git vs Related Tools
| Tool | What it is |
|---|---|
| Git | Version control system (local and remote) |
| GitHub | Cloud hosting for Git repos + collaboration |
| GitLab | Alternative to GitHub with built-in CI/CD |
| Bitbucket | Atlassian's Git hosting (integrates with Jira) |
| Gitea | Self-hosted open-source GitHub alternative |
| SVN | Older centralized version control (not Git) |
| Mercurial | Alternative to Git (less common) |
| Git LFS | Git extension for large files (videos, models) |
| pre-commit | Framework for Git pre-commit hooks |
| GitKraken/SourceTree | Visual GUI clients for Git |
Learning Path
| Stage | What to do | Time |
|---|---|---|
| Beginner | Install Git, init/add/commit/log | Week 1 |
| Basic workflow | push/pull/clone, branches, merge | Week 2-3 |
| Collaboration | conflicts, pull requests, fork | Week 4-5 |
| Intermediate | rebase, stash, tags, .gitignore | Month 2 |
| Advanced | cherry-pick, bisect, hooks, reflog | Month 3+ |
| Expert | custom workflows, aliases, scripting | Ongoing |
Practice resources:
git help <command>— built-in docs- Learn Git Branching (interactive visualization)
- Oh My Git! (card game for learning Git)
- Pro Git book (free at git-scm.com/book)
FAQ
Do I need GitHub to use Git? No. Git is local software. GitHub is a hosting platform built on top of Git. You can use Git entirely on your own machine. GitHub becomes necessary when collaborating or backing up to the cloud.
What's the difference between git pull and git fetch?
git fetch downloads changes from the remote but doesn't touch your local branches. git pull is git fetch followed by git merge. Use git fetch to see what's changed before applying it; use git pull when you want to sync immediately.
When should I use merge vs rebase?
Use merge when combining completed feature branches into main — it preserves the full history. Use rebase to clean up your local commits before sharing them (interactive rebase to squash WIP commits). Never rebase branches other people are using.
How do I undo a git push?
If you pushed by mistake and no one has pulled yet: git push --force-with-lease origin branch-name after reverting locally. If others have already pulled, use git revert to create an undo commit — never force-push to shared branches.
Should I commit package-lock.json / yarn.lock?
Yes, always. Lock files ensure every developer installs the exact same dependency versions. Commit them, but add node_modules/ to .gitignore (don't commit the actual packages).
How large should commits be? Each commit should represent one logical, complete change that passes tests on its own. Not so small it's noise (fixing a typo in a comment = one commit, not three), not so large it's impossible to review (don't combine feature + refactor + bug fix). "If you can explain the commit in one sentence, it's the right size."