I once force-pushed to main during a demo. The CTO was watching. My face is still red.
Here are the Git commands and patterns that would have saved me from every embarrassing moment in my career. Not the basics — the ones you discover only after you've already broken something.
1. git reflog — The Undo Button Nobody Told You About
You ran git reset --hard and your work is gone. You merged the wrong branch. You rebased and everything exploded.
git reflog remembers everything. Every commit you've been on in the last 90 days, even ones that are no longer on any branch.
$ git reflog
a1b2c3d HEAD@{0}: reset: moving to HEAD~3 ← your reset
f4e5d6c HEAD@{1}: commit: add payment flow ← your "lost" work
8g9h0i1 HEAD@{2}: commit: fix auth bug
# Recover your work:
$ git checkout -b recovery f4e5d6c
# Or: git reset --hard f4e5d6c
The rule: If you did it in the last 90 days and Git was involved, reflog can get it back. It's saved my bacon at least 5 times.
2. git stash (The Advanced Version)
Everyone knows git stash. But most people use it like a junk drawer — stuff goes in, never comes out.
# Name your stashes (you will forget what's in them otherwise)
$ git stash push -m "WIP: payment refactor, needs testing"
# See all stashes with their names
$ git stash list
stash@{0}: On main: WIP: payment refactor, needs testing
stash@{1}: On feature: debug logging for auth issue
# Apply a specific stash without removing it
$ git stash apply stash@{1}
# Stash only specific files
$ git stash push -m "just the migration" -- db/migrations/
# Stash including untracked files (new files you haven't git add'd)
$ git stash push -u -m "includes new files"
Pro tip: git stash silently drops untracked files by default. You create a new file, stash, and it's gone. Use -u to include untracked files.
3. git bisect — Let Git Find the Bug For You
Something broke but you don't know which commit caused it. There are 200 commits since it last worked. Don't read them all.
# Start bisect
$ git bisect start
# Mark current commit as bad
$ git bisect bad
# Mark the last known good commit
$ git bisect good v2.3.0
# Git checks out a commit halfway between
# Test it. Then tell Git:
$ git bisect good # if this commit works
$ git bisect bad # if this commit is broken
# Git narrows down, checking out the next midpoint
# Repeat until it finds the exact commit
# Automated version (if you have a test):
$ git bisect start HEAD v2.3.0
$ git bisect run npm test
# Git runs the test on each commit automatically and finds the culprit
git bisect run is magic. I had a CSS regression that appeared somewhere in 150 commits. bisect run found it in 3 minutes: a dependency update that changed a default margin.
4. git commit --fixup and git rebase -i --autosquash
You're working on a feature branch. You notice a typo in a commit from 3 commits ago. Don't create a new commit called "fix typo."
# Fix the typo, then:
$ git add .
$ git commit --fixup a1b2c3d # the commit with the typo
# Later, before merging:
$ git rebase -i --autosquash main
# Git automatically moves the fixup commit next to the original
# and marks it as "fixup" — it'll be squashed in
Your git history stays clean. No "fix typo" commits, no "address review comments" commits. When someone reads the history in 2 years, they see features and fixes — not your editing process.
Set it as default behavior:
$ git config --global rebase.autosquash true
5. git worktree — Work on Two Branches Simultaneously
You're deep in a feature branch. An urgent bug comes in. You need to switch to main, but your working directory is a mess.
Don't stash. Don't commit half-finished work. Use worktrees.
# Create a new working directory for the hotfix
$ git worktree add ../hotfix-branch main
$ cd ../hotfix-branch
# Fix the bug, commit, push — all without touching your feature branch
$ git commit -am "fix: critical auth bypass"
$ git push origin main
# Go back to your feature work
$ cd ../my-project
# Clean up when done
$ git worktree remove ../hotfix-branch
Each worktree is a separate directory with its own checked-out branch, sharing the same .git repository. No stashing, no context-switching overhead. You literally have two branches open in two terminal windows.
6. git log That Actually Helps
The default git log output is useless for understanding what happened. Fix it:
# Visual branch graph
$ git log --oneline --graph --all --decorate
# Find commits that changed a specific file
$ git log --oneline -- src/auth/login.ts
# Find commits by message content
$ git log --oneline --grep="payment"
# Find commits that added or removed a specific string
$ git log -S "calculateDiscount" --oneline
# Find who last modified each line (beyond basic blame)
$ git log -p -S "API_KEY" -- src/config.ts
# Shows every commit that added or removed "API_KEY" in that file
# My alias (add to ~/.gitconfig):
[alias]
lg = log --oneline --graph --all --decorate -20
who = log --oneline --format='%h %an %s' -20
changes = log --oneline --stat -10
The -S flag (the "pickaxe") is criminally underused. "When was this function added?" "When did we start using this library?" "When did this config value change?" All answerable instantly.
7. git cherry-pick (Without the Mess)
You need one specific commit from another branch. Not a merge — just that one commit.
# Grab a specific commit
$ git cherry-pick a1b2c3d
# Grab it without committing (so you can modify it)
$ git cherry-pick --no-commit a1b2c3d
# Cherry-pick a range
$ git cherry-pick a1b2c3d..f4e5d6c
# If there's a conflict, fix it and continue
$ git cherry-pick --continue
# Or abort if it's a mess
$ git cherry-pick --abort
When to use it: Backporting a fix to a release branch. Pulling a specific feature commit into a hotfix branch. Recovering a commit from a deleted branch (combine with reflog).
When NOT to use it: As a replacement for merging. Cherry-picking creates duplicate commits, and if you later merge the source branch, Git may get confused.
Bonus: The .gitconfig That Saves Time
[alias]
# Quick status
s = status -sb
# Undo last commit (keep changes staged)
undo = reset --soft HEAD~1
# Amend without editing message
amend = commit --amend --no-edit
# Show what you've done today
today = log --oneline --since='6am' --author='Your Name'
# Delete merged branches
cleanup = !git branch --merged main | grep -v main | xargs -r git branch -d
# Interactive rebase on the last N commits
ri = "!f() { git rebase -i HEAD~${1:-5}; }; f"
[pull]
rebase = true
[push]
default = current
autoSetupRemote = true
[rerere]
enabled = true # Remember resolved conflicts
The rerere setting is game-changing. If you resolve a merge conflict and then need to redo the same rebase/merge later, Git automatically applies the same resolution. No re-resolving the same conflict 5 times during a long rebase.
What's your most embarrassing Git moment? I promise mine (force-pushing to main during a live demo) is worse. Share in the comments so we can all learn from each other's pain.
Top comments (0)