Skip to main content
Testing, Quality & Craft
🧪 Testing & CraftLesson 11 of 13

Git, Deeply

Git beyond save: branches, rebase, conflict resolution, and bisect to hunt bugs.

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:

  1. Pull fresh main, create a feature branch (feature/ or fix/ prefix).
  2. Make at least two atomic commits using Conventional Commits messages. Use git add -p if you touched more than one concern in a file.
  3. Before merging, run git log --oneline feature/your-branch ^main — every message should be a sentence you'd explain to a teammate.
  4. Merge or open a PR, resolve any conflict if one appears, verify the result on main.
  5. Bonus: introduce a deliberate typo, commit it, fix it, then use git bisect to find the "bad" commit — even a tiny history cements the muscle memory.
  6. Bonus: run git revert HEAD on 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 log review 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 revert vs reset --hard and why
  • Your .gitignore covers node_modules/, .env*, and your build output folder, and no secrets appear in git status
  • You ran at least one git bisect session and finished with git bisect reset

➡️ Next: Working With AI Like a Senior Engineer.

Build It Right, Or Don't Build It At All. 🏛️

Always-on rigor toolkit

🏛️ Build It Right, Or Don't Build It At All.