There's a moment almost every developer hits early on. You've been using Git for a few weeks. You can git add, git commit, and git push without looking anything up. Then a teammate says something like, "Just rebase your branch onto main and force-push," and the ground drops out from under you. You nod, quietly panic, and go copy-paste commands from a forum, hoping nothing explodes.

If that's you, you're in good company. Git is one of those tools almost everyone uses and almost no one feels they truly understand. The good news: you don't need to memorize forty commands. You need a clear mental model of what Git is actually doing. Once that clicks, the commands stop feeling like magic spells and start feeling like obvious moves.

Git isn't a backup tool that happens to track changes. It's a tiny database of snapshots, and every command is just a way of moving through that database.

Let's build that model from the ground up.

What Git Is Really Storing

The single most useful thing to understand about Git is that it does not store your project as a list of changes. It stores snapshots. Every time you commit, Git takes a picture of every tracked file in your project at that moment and saves it. If a file didn't change, Git just points to the previous copy instead of duplicating it, which is why this stays efficient even over thousands of commits.

Each snapshot is a commit, and each commit knows which commit came before it. String them together and you get a chain — your project's history, one photo at a time. A commit also carries a message, an author, a timestamp, and a unique ID (that long string of letters and numbers like a3f9c2e). That ID is how Git refers to any point in history.

This is why the advice "commit early, commit often" matters. Each commit is a save point you can return to. A messy history of small commits is far more useful than three giant commits, because small snapshots give you precise places to jump back to when something breaks.

# See the chain of snapshots, most recent first
git log --oneline

# a3f9c2e Fix login redirect bug
# 7b1d4a0 Add password reset form
# 2e8f6c1 Set up user model

Read that output as a timeline. The top is now; the bottom is where this branch began. Everything else in Git is about navigating and reshaping this timeline.

Branches Are Just Sticky Notes

Here's the idea that unlocks half of Git: a branch is not a copy of your files. It's just a lightweight pointer to one specific commit. Think of it as a sticky note with a commit ID written on it.

When you're on the main branch and you make a commit, Git creates the new snapshot and then moves the main sticky note forward to point at it. When you run git branch feature-login, you're not duplicating anything — you're slapping a second sticky note on the current commit. That's why creating branches in Git is instant, even in a massive repository. You're writing one line, not copying gigabytes.

git branch feature-login      # create a new sticky note here
git switch feature-login      # move onto it (modern syntax)
# ...make commits...
git switch main               # hop back to main, untouched

The special pointer called HEAD is just Git's way of remembering which branch you're currently standing on. When people say "detached HEAD" and it sounds terrifying, all it means is that HEAD is pointing directly at a commit instead of at a branch — you've stepped off the sticky note and onto a bare snapshot. Nothing is broken; you've just wandered somewhere without a label.

Once you see branches as cheap pointers rather than heavy copies, the whole workflow of "branch per feature, merge when done" stops feeling like ceremony and starts feeling like what it is: moving sticky notes around.

Merge vs. Rebase, Without the Fear

Eventually two branches diverge and you need to bring them back together. There are two ways to do it, and the difference is mostly about the story your history tells.

A merge takes both branches and ties them together with a new commit that has two parents. Your history keeps its actual shape — you can see that a feature branch existed and when it rejoined main. It's honest and non-destructive, which is why it's the safe default, especially on shared branches.

A rebase does something sneakier: it lifts your branch's commits off and replays them one by one on top of another branch, as if you'd started your work from there in the first place. The result is a clean, straight line with no merge commits. The cost is that rebasing rewrites commit IDs — those replayed commits are technically new — which is exactly why it's risky on branches other people share.

# Merge: preserves the branching shape
git switch main
git merge feature-login

# Rebase: replays your commits for a linear history
git switch feature-login
git rebase main

The rule of thumb that saves people the most pain: rebase your own local branch to tidy it up before sharing, but never rebase a branch other people are already building on. When you rebase shared history and force-push, everyone else's copy suddenly disagrees with yours, and untangling that is genuinely miserable. Merge in public, rebase in private, and you'll sidestep most of Git's scariest situations.

The Three Areas Your Files Live In

A lot of confusion around git add comes from not realizing your changes pass through three distinct places. Picture them as three trays on a desk.

The working directory is your actual project folder — the files you edit. The staging area (sometimes called the index) is a holding tray where you place the exact changes you want in your next commit. The repository is the permanent record, where committed snapshots go to live forever.

git status                 # what's changed, and what's staged
git add file.py            # move a change into the staging tray
git commit -m "message"    # seal the staged tray into a snapshot

Why the middle tray at all? Because it lets you craft a commit deliberately instead of dumping everything at once. Say you fixed a bug and also tweaked some unrelated styling. You can stage only the bug fix, commit it with a clear message, then stage and commit the styling separately. Two clean commits instead of one muddy one. The staging area is Git handing you a scalpel instead of a shovel.

The Commands That Save You When Things Go Wrong

Most Git panic comes from a handful of "oh no" moments, and each has a calm answer once you know it exists.

You committed to the wrong branch, or want to undo your last commit but keep the changes? git reset --soft HEAD~1 moves the branch pointer back one step and drops your changes back into staging, as if the commit never happened but your work is intact. Want to discard a change in a file you haven't committed yet? git restore file.py snaps it back to the last committed version. Need to set aside half-finished work to deal with something urgent? git stash tucks it away and gives you a clean slate, and git stash pop brings it back later.

git reset --soft HEAD~1    # undo last commit, keep the work staged
git restore file.py        # throw away uncommitted edits to a file
git stash                  # hide messy work-in-progress temporarily
git stash pop              # bring the stashed work back

The one to treat with respect is git reset --hard, which throws away changes permanently. Use it only when you genuinely want work gone. And here's a reassuring secret: almost nothing in Git is truly lost. Even "deleted" commits usually linger in a log called the reflog (git reflog) for a couple of weeks, so a wrong move is very often recoverable. Git is far more forgiving than its reputation suggests.

Putting It All Together

Strip away the intimidating vocabulary and Git is a small set of ideas working together. It stores your project as a chain of snapshots. Branches are cheap pointers to those snapshots, and HEAD marks where you're standing. Merging joins histories honestly while rebasing rewrites them into a clean line — so you merge in public and rebase in private. Your changes flow through three trays — working directory, staging, repository — and a few recovery commands quietly rescue you when you slip.

You don't need to master everything at once. Pick up the recovery commands when you next make a mess, try a rebase on a throwaday branch where nothing's at stake, and let the mental model do the heavy lifting. The developers who seem fluent in Git aren't memorizing more commands than you — they just stopped seeing it as magic. Once you can picture the snapshots and the sticky notes, so will you.