Git — Fundamentals
Analogy — Staging is like packing a box before mailing it. Editing a file is like changing an item in your room — it's not going anywhere until you physically put it in the box (git add, i.e. staging). Sealing and mailing the box (git commit) only sends what was actually inside it at that moment — if you change something in your room after packing but before sealing, that change doesn't magically end up in the box. You'd have to notice, and pack it again.
The Three Trees: Working Directory, Staging Area, Repository
Git tracks changes through three distinct states, and most early confusion about "why didn't my commit include that change" comes from not having a clear mental model of them:
git add moves changes from the working directory into staging; nothing else does..git/) — the committed history. git commit takes whatever is currently staged and turns it into a permanent snapshot.A very common mistake: editing a file after git add-ing it, then committing, and being surprised the new edits aren't in the commit — git add staged a snapshot at that moment, not a live link to the file. Re-run git add after any further edits, or git commit -a to auto-stage all tracked-file changes at commit time (this does not pick up brand-new untracked files).
Branches Are Just Pointers
A branch in Git is not a copy of the codebase — it's a lightweight, movable pointer to a specific commit. This is why creating a branch (git branch feature/x) is instant regardless of repository size: it's writing one small pointer file, not copying anything. HEAD is a pointer to whichever branch (or commit, in detached-HEAD state) you currently have checked out — git log walks backward from wherever HEAD currently points.
Merge vs. Rebase — Same Goal, Different History Shape
Both bring changes from one branch into another, but they produce genuinely different history:
git merge feature/x (while on main) creates a new merge commit with two parents, preserving the exact sequence of commits from both branches exactly as they happened. Non-destructive — never rewrites existing commits.git rebase main (while on feature/x) replays your branch's commits one by one on top of the current tip of main, producing a linear history with no merge commit — but it rewrites your branch's commit hashes in the process.The rule that actually matters in practice: never rebase commits that have already been pushed and that anyone else might have based work on. Rebasing rewrites commit hashes — if someone else already pulled the old commits, their history and yours diverge in a way that's genuinely painful to reconcile. Rebasing your own local, not-yet-pushed commits (to clean up history before opening a PR) is safe and common; rebasing a shared branch after others have pulled it is the mistake that causes real team pain.
Undoing Things — Pick the Right Tool for What You Actually Want
Git has several "undo" commands that look similar but do meaningfully different things — using the wrong one is a common source of either lost work or a messy shared history:
revert vs reset: revert is the right tool once something has been pushed and others may already have it — it never rewrites history, just adds a compensating commit. reset rewrites what your branch points to and is only safe for commits that are still purely local.
Stashing — Temporarily Set Aside Uncommitted Work
The common use case: you're mid-way through changes, need to switch branches to fix something urgent, but aren't ready to commit the in-progress work. git stash clears the working directory so the switch is clean, without losing anything or forcing a half-finished commit.
Tags — Marking a Specific Point in History
Use annotated tags (-a) for anything meant to represent a real release — they carry metadata (who tagged it, when, why) that a lightweight tag doesn't, which matters when someone's trying to understand release history months later.
Try It (2 Minutes)
See "branches are just pointers" for yourself:
file.txt on main still says "v1" even though you committed "v2" — because experiment was never a copy of the codebase, just a separate pointer that moved forward on its own while main's pointer stayed exactly where it was. That's the entire mechanic behind why creating a branch is instant, regardless of repo size.

