Git Beyond the Basics
Git as a content-addressable DAG, not a list of commands — rebase vs merge, the reflog safety net, bisect for finding bugs, the three resets, and the workflows that keep history useful.
Git Beyond the Basics
In one line: Once you stop memorizing Git commands and see the model underneath — commits are immutable snapshots in a graph, and branches are just movable pointers — every command becomes obvious and almost nothing is ever truly lost.
You learned add, commit, push, pull early. This is the mental model that turns Git from a scary set of incantations into a tool you wield deliberately — and recover from confidently.
- The basics — init, staging, commit, branch, push/pull, resolving a simple conflict — are in Roadmap Stage 4: Git.
This page is the model and the power tools on top of those basics.
1. The model: commits are a DAG, branches are pointers
Internally, Git is a content-addressable graph. A commit is an immutable snapshot of your whole tree plus a pointer to its parent(s), identified by a hash of its contents. A branch is not a copy of anything — it's just a movable pointer (a 40-character file) to one commit. HEAD points to the branch you're on.
A───B───C ← main (a pointer to commit C)
\
D───E ← feature (a pointer to commit E)
HEAD → feature
This one fact explains everything:
- Creating a branch is instant — it writes a 41-byte pointer file, not a copy.
commitmakes a new snapshot and moves the current branch pointer forward.merge,rebase,resetare all just moving pointers and/or creating commits — nothing is destroyed; you're rearranging the graph.
Internalize "commits = immutable snapshots in a DAG, branches = pointers" and the rest of this page is just consequences.
2. Rebase vs merge — two ways to combine history
Both integrate one branch's work into another; they differ in the history they produce:
- Merge creates a new "merge commit" that ties the two histories together. History is truthful (it shows the branch existed) but can get tangled with many merge commits.
- Rebase replays your commits on top of the target branch, as if you'd started from its tip. History is linear and clean, but you've rewritten your commits (new hashes).
# Clean up your own local feature branch before opening a PR:
git rebase -i main # squash WIP commits, reword messages, reorder — a tidy story
# Integrate a finished feature into a shared branch:
git merge --no-ff feature # explicit merge commit; preserves that "feature" happened
Never rebase commits that other people have already pulled. Rebase rewrites history (new commit hashes); if those commits are public, everyone else's history now diverges from yours, producing duplicate commits and ugly conflicts on their next pull. Safe rule: rebase local, unpushed work to clean it up; merge to integrate shared branches. Rewrite your own private history freely; never rewrite shared history.
3. The reflog — your safety net (nothing is lost)
The single most reassuring thing to know: Git almost never deletes commits. Even after a "disastrous" reset --hard or a botched rebase, the old commits still exist — they're just no longer pointed to. The reflog records every position HEAD has been:
git reflog # a timeline of everywhere HEAD has pointed
# ...
# a1b2c3d HEAD@{2}: reset: moving to HEAD~3 ← the "disaster"
# e4f5g6h HEAD@{3}: commit: the work I thought I lost
git reset --hard e4f5g6h # restore HEAD to that commit — work recovered
Unreferenced commits stick around for ~90 days before garbage collection. So when someone says "I lost my work in Git," the answer is almost always "no you didn't — git reflog." Knowing this turns Git from terrifying to fearless: you can experiment with history because you can always get back.
4. Bisect — binary-search your history for the bug
A bug appeared "sometime in the last 200 commits" and you don't know which. git bisect does a binary search through history — ~8 checkouts for 200 commits instead of 200 — to find the exact commit that introduced it:
git bisect start
git bisect bad # current commit is broken
git bisect good v1.4.0 # this old release was fine
# Git checks out the midpoint; you test and mark good/bad; it narrows in.
# Fully automate it with a test script that exits non-zero on failure:
git bisect run npm test
# → "abc123 is the first bad commit" + the diff that caused it.
This is the fastest way to answer "what change broke this?" on a large project — and a reason to keep commits small and atomic (so the culprit commit is a small, readable diff).
5. The three resets (and restore)
reset confuses people because it does three different jobs depending on the flag — it's about which of the three areas (HEAD/commit, staging index, working tree) it touches:
| Command | Moves branch pointer | Resets staging | Resets working files | Use for |
|---|---|---|---|---|
reset --soft HEAD~1 | yes | no | no | undo last commit, keep changes staged |
reset --mixed HEAD~1 (default) | yes | yes | no | undo commit + unstage, keep edits |
reset --hard HEAD~1 | yes | yes | yes | discard everything (recoverable via reflog) |
Modern Git also splits the friendlier verbs out of the overloaded checkout: git switch changes branches, git restore restores file contents. Use those for clarity; reach for reset when you're moving the branch pointer itself.
6. The targeted power tools
git cherry-pick <hash>— copy one specific commit onto your current branch (e.g. hotfix a release branch with a fix from main).git stash— shelve uncommitted changes to switch context, thenstash popto restore. (Don't let stashes pile up — they're easy to forget.)git worktree— check out multiple branches into separate directories simultaneously (review a PR while keeping your WIP untouched) without cloning twice.
7. Workflows: keep history a useful story
Branching strategy is a team decision, but the modern default for shipping fast is trunk-based development: short-lived feature branches merged into main frequently (daily), behind feature flags for incomplete work. It beats long-lived git-flow branches that drift and produce merge hell. Supporting habits: small, focused PRs (easy to review, easy to bisect), atomic commits (each commit builds and does one thing), and conventional commit messages (feat:, fix:, chore:) that make history scannable and can auto-generate changelogs. This is the version-control half of the CI/CD lifecycle.
Why this matters for you
Git fluency compounds: a few hours understanding the model saves you from the weekly "I'm stuck and afraid to touch it" moments, lets you keep a clean, bisectable history that makes future debugging fast, and means you can recover from any mistake instead of re-cloning and copy-pasting files. It's the tool you use more than any other — being fearless with it pays back forever.
Going further (optional)
This page plus the First step below are self-contained — you don't need to leave the site to learn or apply this. If you want to go even deeper, these are the best external resources:
The free Pro Git book (chapters on branching and "Git internals" are the gold). Learn Git Branching for a visual, interactive feel of how commands move the graph. Then: rebase-clean your next feature branch before the PR, and deliberately "lose" a commit with reset --hard and recover it with reflog so the safety net feels real.
First step
In any repo, run git reflog and read it — see the breadcrumb trail of everywhere HEAD has been. Then start your next feature on a branch, make a few messy WIP commits, and git rebase -i main to squash them into one clean commit with a good message before you push. You'll feel the difference between "Git happens to me" and "I shape history on purpose."
Common mistakes
- Rebasing shared/public history. Rewriting commits others have pulled creates duplicate commits and conflicts for everyone. Rebase local unpushed work; merge to integrate shared branches.
- Thinking
reset --hardlost your work forever. It almost never does —git reflogfinds the orphaned commit for ~90 days. Fear of Git usually comes from not knowing this. - Giant, mixed commits. "Fix stuff" commits touching 40 files can't be reviewed, reverted, or bisected cleanly. Keep commits small and atomic — each one builds and does one thing.
- Long-lived feature branches. They drift from
mainfor weeks and become a merge nightmare. Prefer short-lived branches merged daily, with feature flags hiding unfinished work. - Memorizing commands without the model. You'll be lost the moment something unusual happens. Learn "commits are snapshots in a DAG, branches are pointers" and the commands become derivable.
- Committing secrets and trying to delete the commit. Removing it from history doesn't help (it's in clones/caches) — rotate the secret. (See Security.)