Git, Deeply
Stage 3 · Testing, Quality & Craft · B.U.I.L.D. letter: D
You already know how to commit and push. That makes you dangerous in the best way. Now let's make you trustworthy — to your future teammates, to the codebase, and to yourself at 2 a.m. six months from now.
⚠️ The vibe trap
When you're moving fast, it's tempting to dump everything into one commit called "stuff" and force-push straight to main. That works — right up until a bug slips in, a teammate's work disappears, or you need to undo exactly one thing and can't find it. Branches feel like overhead until the day they save you three hours of archaeology. Good Git hygiene isn't gatekeeping — it's writing a history you can actually use.
🌿 Branches and a Sane Workflow
git switch main
git pull origin main # always start from a fresh main
git switch -c feature/add-dark-mode
# do your work, commit as you go, then:
git push -u origin feature/add-dark-mode
# open a PR — never push features directly to main
Mental model: main is the living product. Branches are workbenches. You make a mess on the workbench, clean it up, then carry the finished piece back. Keep branches short-lived — hours or a day or two, not weeks — and they stay small, review fast, and conflict rarely.
Common mistake: Keeping a branch alive for two weeks while rebasing from main. Cut smaller slices. If a feature is big, build it behind a feature flag and merge incrementally.
🔀 Merge vs. Rebase — and the Golden Rule
# Merge: joins two lines of work, preserves full history
git switch main && git merge feature/add-dark-mode
# Rebase: replays your commits on top of the target, linear history
git switch feature/add-dark-mode
git rebase main
git switch main && git merge --ff-only feature/add-dark-mode
Mental model: Merge is a traffic intersection — you see both roads arriving. Rebase is a revised manuscript — someone cleaned up the chapter order.
The Golden Rule: Never rebase a branch other people have already based work on. When you rewrite history, their commits lose the shared base. Safe rebase targets: your own local feature branch before you've pushed it, or after confirming nobody else is on it.
Common mistake: Running git pull --rebase without realizing you're rewriting local commits. Fine when the branch is yours alone; risky otherwise.
⚔️ Resolving Merge Conflicts Calmly
git switch main && git pull origin main
git merge feature/add-dark-mode
# CONFLICT (content): Merge conflict in src/theme.ts
# Inside the file you'll see:
# <<<<<<< HEAD
# background: "#ffffff"; ← what main currently has
# =======
# background: "#1a1a2e"; ← what your branch added
# >>>>>>> feature/add-dark-mode
# Edit to the version you want, delete ALL markers, then:
git add src/theme.ts
git commit # Git pre-fills the merge message — accept it
Mental model: <<<<<<< to ======= is HEAD (the branch you're merging into). ======= to >>>>>>> is yours. Everything outside the markers is agreed-upon. Your job: decide, delete all markers, save, stage, commit.
Common mistake: Leaving one ======= in the file. The build breaks in a confusing way. Search for <<<< before you commit.
📝 Commit Hygiene
One commit = one logical change — not one file, not one day's work, one idea. Conventional Commits style makes your log readable and your changelogs automatic.
# <type>(<scope>): <imperative-mood summary>
git commit -m "feat(auth): add Google OAuth login button"
git commit -m "fix(cart): prevent duplicate items on rapid double-click"
git commit -m "test(checkout): add integration test for empty-cart edge case"
git commit -m "chore: add .env to .gitignore"
# Stage only part of a file when two concerns changed at once
git add -p src/app.ts # interactive hunk-by-hunk staging
Mental model: Your commit log is documentation. feat(auth): add Google OAuth tells a reviewer exactly what to look for. "wip stuff fix more stuff" tells nobody anything.
Common mistake: Committing console.log lines, .env files, or half-finished work. Use git add -p to be deliberate about what goes in.
🔁 The Undo Toolbox
Three tools, not interchangeable. Pick the wrong one and you delete work or rewrite shared history.
# git revert — SAFE everywhere, even on shared branches
# Creates a NEW commit that undoes the target. History stays intact.
git revert HEAD # undo last commit, keep history
git revert a3f9c12 # undo any commit by hash
# git reset — rewrites history; ONLY safe on local/private branches
git reset --soft HEAD~1 # undo last commit, keep changes staged
git reset --mixed HEAD~1 # undo last commit, changes back to unstaged (default)
git reset --hard HEAD~1 # undo last commit AND discard changes — gone for good
# git restore — discards working-tree edits, does NOT touch commits
git restore src/broken-file.ts # throw away unstaged edits to one file
git restore --staged .env.local # unstage a file without losing its content
Mental model: revert = politely undo in public. reset --soft/--mixed = private revision. reset --hard = eraser — intentional only. restore = ctrl+z for file edits.
Common mistake: Reaching for reset --hard first because it sounds decisive. Default to revert when in doubt. The irreversible thing can wait until you're sure.
🔬 Git Bisect — Find the Bug Commit
A regression appeared and you don't know when. git bisect binary-searches your history and finds the exact commit that introduced it in O(log n) steps.
git bisect start
git bisect bad # current HEAD is broken
git bisect good v1.4.0 # this tag or hash was known-good
# Git checks out a commit halfway between. Test your app. Bug present?
git bisect bad # yes → Git narrows toward earlier commits
git bisect good # no → Git narrows toward later commits
# Repeat until:
# a3f9c12 is the first bad commit
git bisect reset # returns you to your original branch
Mental model: Binary search on time. Ten commits? Two or three answers. A hundred? Seven. A thousand? Ten. This skill makes you look like a wizard to anyone who's never seen it.
Common mistake: Forgetting git bisect reset. You'll be stuck in detached HEAD staring at old code wondering why everything looks wrong.
🙈 .gitignore and Never Committing Secrets
Once a secret is committed and pushed, removing it from history is painful — and it's already in the logs of everyone who cloned the repo. Prevention is the entire strategy.
# .gitignore (root of repo)
node_modules/
.env
.env.local
.env*.local
dist/
build/
.DS_Store
*.log
coverage/
.next/
# Accidentally staged a secret before committing?
git restore --staged .env.local
# Already committed but NOT pushed?
git reset HEAD~1 # back off the commit, fix .gitignore, re-commit
# Committed AND already tracked but not pushed?
git rm --cached .env # removes from index, NOT from disk
Why it matters: A .env with a Stripe secret pushed to a public repo is a real incident. API keys get scraped by bots within minutes of appearing on GitHub.
Common mistake: Adding .env to .gitignore after already committing it. The file is still tracked. You must git rm --cached .env to untrack it, then commit the change.
📦 Git Stash — The Interruption Drawer
You're mid-feature and need to switch branches right now. Changes aren't commit-ready.
git stash push -m "wip: dark mode toggle not wired up yet"
git switch main # do the urgent thing, come back
git stash list # see everything in the drawer
git stash pop # apply most recent stash and remove it
git stash apply stash@{1} # apply a specific stash (keeps it in the list)
git stash drop stash@{1} # delete a stash you no longer need
Mental model: It's a clipboard, not a commit. Use it for short interruptions. If you've stashed the same thing for three days, commit it as WIP on a branch — stashes don't show in git log and are easy to forget.
Common mistake: Popping a stash weeks later on top of incompatible changes. Name your stashes, pop them same day when possible.
🛠️ Your Mission
Pick a real feature or fix and run the full workflow:
- Pull fresh
main, create a feature branch (feature/orfix/prefix). - Make at least two atomic commits using Conventional Commits messages. Use
git add -pif you touched more than one concern in a file. - Before merging, run
git log --oneline feature/your-branch ^main— every message should be a sentence you'd explain to a teammate. - Merge or open a PR, resolve any conflict if one appears, verify the result on
main. - Bonus: introduce a deliberate typo, commit it, fix it, then use
git bisectto find the "bad" commit — even a tiny history cements the muscle memory. - Bonus: run
git revert HEADon a commit, check the revert commit, confirm the change is gone without history being rewritten.
✅ You're Done When…
- You have a Pre-Ship Checklist that includes a
git logreview before every merge - Your feature branch was created fresh off
main, kept short-lived, and merged with clean Conventional Commits messages - You can explain — without looking it up — when to use
revertvsreset --hardand why - Your
.gitignorecoversnode_modules/,.env*, and your build output folder, and no secrets appear ingit status - You ran at least one
git bisectsession and finished withgit bisect reset
➡️ Next: Working With AI Like a Senior Engineer.
Build It Right, Or Don't Build It At All. 🏛️