A Practical Introduction

Git
from zero
to pro

Version control, branching, rebase, remotes, and the beautiful graph underneath it all.

commits branches remotes rebase DAG
press → or arrow-keys to advance
concept 01

What is Git & why does everyone use it?

Created by Linus Torvalds in 2005 to manage the Linux kernel. Now the world's most-used VCS.

The Brief History of Version Control 1972 — SCCS Source Code Control System. Single file locking. 1986 — CVS Centralised. Branching, but painful. 2000 — Subversion (SVN) Better centralised model. Still needs a server. 2005 — Git Distributed. Fast. Designed for Linux kernel scale. Written by Linus Torvalds in ~10 days. 2008 — GitHub launches Git adoption explodes globally. 2026 — Git today ← you are here ~95% of developers. 500M+ repos on GitHub. Native to every IDE, CI/CD, and cloud platform. SHA-256 migration underway. AI tooling integrated.

🌍 Why Git won

  • Distributed — full repo on every machine, no central server required
  • Fast — most ops are local, no network round-trip
  • Branching is cheap — just a 41-byte file
  • Cryptographic integrity — SHA-1/SHA-256 hashes everything

📊 By the numbers

  • Used by ~95% of developers
  • 100M+ repos on GitHub alone
  • Powers Linux, Chromium, Android, VS Code…
  • Every major cloud CI/CD system is Git-native

🔑 Core idea

Git doesn't store diffs — it stores snapshots. Each commit is a complete picture of your project at that moment, referenced by a hash.

concept 02

git init & git config

Starting a new repo and telling Git who you are.

# Create a new repository in current directory
git init

# Git creates a hidden .git/ folder:
#  .git/
#  ├── HEAD          ← points to current branch
#  ├── config        ← repo-local settings
#  ├── objects/      ← all your data (blobs, trees, commits)
#  └── refs/         ← branches and tags

# ─────────────────────────────────────────
# Tell Git who you are (stored in ~/.gitconfig)
git config --global user.name  "Ada Lovelace"
git config --global user.email "[email protected]"

# Set your preferred editor (for commit messages)
git config --global core.editor "vim"

# Useful: always rebase on pull (recommended)
git config --global pull.rebase true

# Check your config
git config --list

# Scope: --global (user), --local (repo), --system

📁 What .git/ contains

  • HEAD — which branch/commit you're on
  • objects/ — every blob, tree and commit ever made
  • refs/heads/ — your local branches (just hash files!)
  • refs/remotes/ — remote tracking branches
  • config — repo-local settings override global

⚠️ Identity matters

Your name and email are baked into every commit permanently. Set them correctly before your first commit — changing them later in shared repos is painful.

🔁 Clone vs Init

git clone <url> is just git init + fetch + checkout, already configured with the remote.

concept 03

Working Area · Staging · Committed

Git has three distinct zones. Understanding them is the key to everything.

Working Directory files on disk — your editor sees this 📄 main.py 📄 utils.py 📄 README.md (untracked) Modified files show up in: git status red = unstaged changes git add git add -p (interactive) Staging Area (Index) .git/index — snapshot of next commit 📄 main.py ✓ staged 📄 utils.py ✓ staged Staged files show in: git status (green) git restore --staged git commit -m "message" Repository (.git/) commit graph — permanent history a1b2c3 init d4e5f6 feat A 7g8h9i HEAD Each commit is a full snapshot identified by a SHA hash. History is immutable — you can always go back. git checkout / restore your edits what will be committed permanent snapshots
concept 04

git add & git commit

Moving work from your editor into permanent history.

# Stage a specific file
git add src/main.py

# Stage everything in current directory
git add .

# Stage interactively — pick hunks, not whole files
git add -p

# See what's staged vs unstaged
git status
git diff           # unstaged changes
git diff --staged  # staged changes (what will be committed)

# ─────────────────────────────────────────
# Commit staged files
git commit -m "feat: add user authentication"

# Stage all tracked files AND commit in one step
git commit -am "fix: correct off-by-one in parser"

# Amend the last commit (before pushing!)
git commit --amend

# Open editor for multi-line commit message
git commit

💡 git add -p is your friend

Interactively stage hunks within a file. Make one commit per logical change — even if you edited multiple things in one session. Clean history = clean team.

📝 Good commit message format

  • type: short summary (≤72 chars)
  • Types: feat · fix · docs · refactor · test · chore
  • Blank line, then optional body with why
  • Write in imperative: "add" not "added"

🔄 git commit --amend

Rewrites the last commit (new hash!). Safe to use before pushing. After pushing, coordinate with your team first.

concept 05

Branches & git checkout -b

A branch is just a lightweight pointer to a commit. Creating one costs almost nothing.

Branch = pointer to a commit main A B C branch point feature/login D E HEAD F main $ cat .git/refs/heads/feature/login e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4 ← just a file containing a 40-char SHA hash. That's it! $ git checkout -b feature/login # create + switch $ git switch -c feature/login # modern equivalent $ git branch # list branches $ git switch main # switch back

🌿 Branch naming conventions

  • feature/user-login
  • fix/null-pointer-crash
  • chore/update-deps
  • release/2.4.0

✅ Branch early, branch often

Creating a branch is instantaneous and costs 41 bytes. There is zero reason not to branch for every feature, fix, or experiment.

🧭 HEAD explained

HEAD is a pointer to your current branch, which itself points to a commit. Moving between branches just updates HEAD — your files update to match.

concept 06

git log — reading history

Exploring the commit graph. Pretty-print makes it actually readable.

# Basic log
git log

# One line per commit
git log --oneline

# Pretty graph — the most useful alias you'll ever set
git log --oneline --graph --all --decorate

# Example output:
* e5f6a7b (HEAD -> feature/login) add JWT validation
* d4e5f6c add login form
| * 3c4d5e6 (main) hotfix: sanitise inputs
|/
* 2b3c4d5 initial commit

# Set a permanent alias for the above
git config --global alias.lg \
  "log --oneline --graph --all --decorate"
# Then just:
git lg

# Search commits by message
git log --grep="fix"

# Search commits by code change (pickaxe)
git log -S "function parseUser"

# Show changes in a commit
git show d4e5f6c

🌳 The graph view

Always use --graph --all. Without it you only see the current branch. The graph reveals the true structure of your DAG.

🔍 Navigating ranges

  • main..feature — commits in feature not in main
  • HEAD~3 — 3 commits back from HEAD
  • HEAD^ — first parent of HEAD

🔬 git blame

git blame file.py shows which commit last touched each line. Invaluable for understanding why code exists. Not for naming and shaming!

concept 07

The DAG — Directed Acyclic Graph

Git history is not a line. It's a graph. Every commit points to its parent(s). This is the foundation of everything.

a1b2 init c3d4 setup e5f6 base branch point g7h8 hotfix i9j0 main k1l2 feat-A m3n4 feat-B o5p6 merge 2 parents! q7r8 HEAD feature/login main Key DAG properties: • Directed: arrows always point from child to parent • Acyclic: no loops — history never circles back • Merge commits have 2 parents • Every commit is uniquely identified by its SHA hash of content + metadata + parent hash
concept 08

Git Hashes & Why History is Hard to Change

Every commit's hash depends on its content AND its parent's hash. Change one thing, change everything after it.

What goes into a commit hash (SHA-1) SHA1( inputs ) = commit hash tree hash ← snapshot of all files parent hash(es) ← chain to history author + timestamp commit message Change the message of commit B → new hash for B → C's parent hash changes → new C → new D… Before: A B C D After amending B: A B' new! C' new! D' new! everyone who had C or D now diverges!

⚠️ The Golden Rule

Never rewrite history that has been pushed and shared. When you change a commit, everyone who built on top of it gets a divergent branch they must manually reconcile.

✅ Safe history changes

  • Amend the last commit — if not yet pushed
  • Rebase a private feature branch before merge
  • Interactive rebase on local-only commits

🔐 SHA-1 → SHA-256

Git is migrating from SHA-1 to SHA-256 for security. SHA-256 hashes are 64 hex chars. You may see both in modern repos. The integrity guarantee is the same.

concept 09

git rebase — replaying commits

Rebase moves your branch to start from a different point, creating new commits with the same changes but fresh hashes.

BEFORE git rebase main A B C main D E feature ← HEAD ↓ git rebase main replays D and E on top of C AFTER — linear, clean history A B C main D' new hash E' new hash feature ← HEAD D E old commits — still exist briefly GC'd after ~30 days

✅ When to rebase

  • Before merging a feature branch — keep main clean
  • On a local/private branch only
  • To incorporate upstream changes without a merge commit

⛔ Never rebase

  • Shared branches that others have checked out
  • main / develop branch directly
  • After a PR is under review (confuses reviewers)

🔀 Rebase vs Merge

Merge preserves the exact history as it happened. Rebase creates a cleaner linear story. Most teams prefer rebase for feature branches + a single merge commit at the end.

concept 10

Interactive Rebase — rewriting history

git rebase -i lets you squash, reorder, edit or drop commits before sharing your work.

# Interactively rebase the last 4 commits
git rebase -i HEAD~4

# Editor opens with a list like this:
pick a1b2c3d feat: add login form
pick e4f5g6h fix: typo in label
pick i7j8k9l fix: another typo
pick m0n1o2p test: add login tests

# Change "pick" to one of:
#  p pick    — keep commit as-is
#  r reword  — keep changes, edit message
#  e edit    — pause and amend the commit
#  s squash  — meld into previous commit
#  f fixup   — squash, discard this message
#  d drop    — remove the commit entirely

# Clean version: squash the typo fixes
pick a1b2c3d feat: add login form
f    e4f5g6h fix: typo in label
f    i7j8k9l fix: another typo
pick m0n1o2p test: add login tests

# Result: 2 clean commits instead of 4

🎯 The golden workflow

  • Work freely, commit often (even "WIP" commits)
  • Before opening a MR: git rebase -i
  • Squash fixups, reword messages, drop debug commits
  • Push a clean, reviewable history

📝 squash vs fixup

squash opens the editor to combine both commit messages. fixup silently discards the message of the squashed commit — faster for typo fixes.

🚨 Conflict during rebase

  • Fix the conflict in your editor
  • git add <file>
  • git rebase --continue
  • Or bail: git rebase --abort
concept 11

Remote Repos — GitHub & GitLab

Your local repo is complete. Remotes are just other repos you sync with.

🌐 origin github.com / gitlab.com authoritative shared repo refs/remotes/origin/* 💻 Your machine local repo working dir + staging origin/main ← tracking branch always knows remote state 💻 Teammate their local repo same remote, different work origin/main ← tracking branch git push git pull

🔗 Key remote commands

  • git clone <url> — full copy + remote configured
  • git fetch — download remote changes, don't merge
  • git pull — fetch + merge (or rebase) into current branch
  • git push — upload local commits to remote

📥 fetch vs pull

git fetch is always safe — it only updates your remote-tracking branches. git pull also modifies your working branch. When in doubt, fetch first, inspect, then merge.

⬆️ Pushing a new branch

git push -u origin feature/login — the -u sets the upstream so future git push works without arguments.

concept 12

Merge Requests & Merge Types

A Merge Request (GitLab) / Pull Request (GitHub) is a code review + merge in one workflow. Fast-forward keeps history clean.

✅ Fast-Forward Merge main has no new commits since branch Before: A main B C feature After — just moves pointer: A B C main No merge commit. Clean linear history. Merge Commit both branches have diverged A B main C M merge 2 parents Preserves full history. Creates an extra commit. Graph shows diverge + merge. Squash Merge all feature commits → one new commit A B main C D E CDE squashed main Feature commits disappear. One clean commit on main. Loses individual commit context. ⭐ Recommendation: Rebase your branch first so main can fast-forward merge git switch feature → git rebase main → open MR → merge with fast-forward → clean linear history ✅
concept 13

Commit Message Etiquette

A good commit message is a letter to future-you (and your teammates). Make it count.

# ✅ GOOD commit message
feat(auth): add JWT refresh token rotation

Refresh tokens now rotate on each use to prevent
token replay attacks. Old tokens are invalidated
immediately after a successful refresh.

Closes #482
See: https://datatracker.ietf.org/doc/html/rfc6749

# ────────────────────────────────────────
# ❌ BAD commit messages
fix stuff
wip
asdfgh
updated files
it works now
final
final FINAL
ok seriously final this time

📐 The format — Conventional Commits

  • type(scope): summary
  • Summary: imperative mood, ≤72 chars, no period
  • Blank line between summary and body
  • Body: explain why, not what (code shows what)
  • Footer: issue refs, breaking change notices

🏷️ Conventional Commit types

  • feat — new feature (triggers minor version bump)
  • fix — bug fix (triggers patch bump)
  • refactor — no behaviour change
  • docs · test · chore · ci
  • feat!: — breaking change (major bump)

💡 Why it matters

  • Tools like semantic-release auto-version from messages
  • Auto-generate changelogs
  • git log --grep becomes useful
  • git bisect finds bugs faster
concept 14

git stash push & pop

Temporarily shelve work-in-progress so you can switch context without committing.

# Save current changes to the stash stack
git stash push
# (shorthand — same thing)
git stash

# Give it a meaningful name
git stash push -m "WIP: half-done auth refactor"

# Include untracked files too
git stash push -u

# List all stashes
git stash list
# stash@{0}: WIP: half-done auth refactor
# stash@{1}: On main: quick experiment

# Restore most recent stash (and remove from stack)
git stash pop

# Apply without removing from stack
git stash apply stash@{1}

# Apply stash to a new branch
git stash branch feature/auth-wip

# Discard a stash
git stash drop stash@{0}
git stash clear  # drop ALL

🔄 Typical workflow

  • You're mid-feature. Urgent bug reported on main.
  • git stash push -m "WIP login"
  • git switch main — fix the bug, commit, push
  • git switch feature/login
  • git stash pop — back to work

⚠️ Stash caveats

  • Stash is a stack — LIFO. Easy to forget old stashes.
  • Not backed up to remote — local only.
  • For longer breaks, consider a WIP commit instead — then git reset HEAD~1 when returning.

💡 stash is a commit

Under the hood, stash creates real commits in a special ref. You can even git show stash@{0} to inspect what's saved.

concept 15

git worktree — multiple branches at once

Check out multiple branches simultaneously into separate directories — no stashing needed.

# Add a new worktree for a branch
git worktree add ../myapp-hotfix hotfix/critical-bug

# Add worktree for a NEW branch
git worktree add -b feature/payments ../myapp-payments

# List all active worktrees
git worktree list
# /home/ada/myapp           a1b2c3d [main]
# /home/ada/myapp-hotfix    d4e5f6g [hotfix/critical-bug]
# /home/ada/myapp-payments  h7i8j9k [feature/payments]

# Work in each directory independently
# cd ../myapp-hotfix && vim src/bug.py && git commit
# cd ../myapp-payments && vim src/pay.py && git commit

# Remove a worktree when done
git worktree remove ../myapp-hotfix

# All worktrees share the same .git/ objects
# Commits from any worktree appear in all

🌳 When worktrees shine

  • Running two servers locally on different branches
  • Reviewing a PR while mid-feature
  • Long-running build in one branch while coding in another
  • Testing a bug fix against your current WIP

🆚 Worktree vs Stash vs Clone

  • stash — quick context switch, same dir
  • worktree — parallel branches, separate dirs, shared objects
  • clone — completely separate repo copy — no sharing

⚙️ Rules

  • A branch can only be checked out in one worktree at a time
  • All worktrees share .git/ — commits are instantly visible everywhere
recap

You now know Git

From the first commit to rewriting history like a pro.

🌱 Foundations

  • git init / config — identity matters
  • Working → Staging → Committed (3 zones)
  • git add -p for clean, atomic commits
  • Branches are just 41-byte pointer files

🔍 Inspection & History

  • git log --graph --all --oneline
  • The DAG: Directed Acyclic Graph
  • SHA hashes chain history — change one, change all downstream
  • git log -S to find when code appeared

🔀 Rewriting & Sharing

  • git rebase — replay on new base
  • git rebase -i — squash, reword, drop
  • Never rewrite shared history
  • Fast-forward merge = cleanest history

🌐 Remotes

fetch (safe) vs pull (merges). Push -u to set upstream. MRs / PRs are code review + merge. Rebase before merge for fast-forward.

💼 Productivity

git stash for quick context switches. git worktree for parallel branch work. git stash branch to promote a stash to a branch.

📝 Culture

Conventional commits: feat/fix/chore. Explain why in the body. Good history is a gift to future teammates — and future you.

Next: git-scm.com/book · ohshitgit.com · learngitbranching.js.org