I still remember the day a senior engineer on my team committed a single line change and it landed with a perfectly scoped commit message, a clean diff, and zero noise from unrelated files. I had been using Git for two years at that point and I thought I knew it. I did not know it.
That day started a personal obsession with Git that has saved me dozens of hours, impressed multiple teammates, and — once — prevented a production disaster that would have ruined a Monday morning for the entire company.
Here are the tricks I wish someone had shown me on day one.
1. Fixup Commits: The Clean History Superpower
You just pushed a feature branch. Your reviewer replies: "looks good, just fix the typo in the function name." So you make a tiny fix and now your history looks like:
add user authentication
fix tests
wip
fix typo
Hideous. Here is the better way using --fixup:
# Make your fix, then commit it as a fixup to a specific previous commit
git add .
git commit --fixup=HEAD~2
# Then squash everything neatly with autosquash
git rebase -i --autosquash HEAD~4
Git automatically marks the fixup commit and places it right after its target during the interactive rebase. You end up with a clean, logical history with zero manual reordering.
2. git reflog: Your Undo Button for Everything
You did a git reset --hard and lost work. Panic sets in. Stop. Breathe. Open this:
git reflog
You will see something like:
a3f2b1c HEAD@{0}: reset: moving to HEAD~3
7d9e4a2 HEAD@{1}: commit: add payment integration
3c1a8f0 HEAD@{2}: commit: update cart logic
That commit you thought was gone? It is still there. Get it back:
git checkout 7d9e4a2
# or restore your whole branch:
git reset --hard 7d9e4a2
The reflog keeps a record of every place HEAD has pointed for the last 90 days by default. It is your time machine.
3. Partial Staging with git add -p
This one changes how you think about commits entirely. You have been hacking on a feature and also fixed a bug along the way. Both changes are in the same file. Most developers either commit everything together (messy) or manually copy-paste changes (painful).
Instead:
git add -p
Git shows you each "hunk" of changes one at a time and asks what to do:
@@ -14,6 +14,10 @@ function fetchUser(id) {
const user = await db.findById(id);
+ if (!user) throw new Error('User not found');
return user;
}
Stage this hunk [y,n,q,a,d,s,?]?
Hit y to stage, n to skip, s to split the hunk into smaller pieces. You can build a perfectly scoped commit from a file that has five different concerns changed in it.
This is how senior engineers write commits that tell a story.
4. git bisect: Find the Exact Commit That Broke Everything
Something is broken in production. It worked two weeks ago. You have 200 commits between then and now. How do you find the culprit?
Most people start git log scrolling and guessing. The right answer is binary search:
git bisect start
git bisect bad # current commit is broken
git bisect good v2.4.0 # this tag was working
Git checks out the midpoint commit. You test it. Then:
git bisect good # if it works
git bisect bad # if it is broken
Repeat. Git narrows it down in log2(N) steps. For 200 commits, that is about 8 rounds. You find the exact breaking commit in minutes.
You can even automate it with a test script:
git bisect run npm test
Git runs your test suite at each midpoint and determines good/bad automatically. Walk away and come back to the answer.
5. Stash with a Name (and Stop Losing Stashes)
Everyone uses git stash. Almost nobody uses it well.
# Bad: unnamed stash you will forget in two days
git stash
# Good: named stash you can find later
git stash push -m "WIP: cart discount logic before refactor"
Now when you list stashes:
git stash list
# stash@{0}: On feature/cart: WIP: cart discount logic before refactor
# stash@{1}: On main: hotfix idea for rate limiter
Apply a specific one by name:
git stash apply stash@{1}
Bonus: stash only your unstaged changes while keeping the index intact:
git stash push --keep-index
Useful when you want to test if your staged changes work in isolation before committing.
6. git worktree: Work on Two Branches Simultaneously
This is the trick that genuinely surprises people. You are deep in a feature branch. A critical hotfix comes in. You need to switch branches but you do not want to stash or lose your mental context.
Instead of switching, add a worktree:
git worktree add ../hotfix-branch hotfix/payment-bug
This checks out hotfix/payment-branch in a separate folder (../hotfix-branch) while your current folder stays on your feature branch. Two branches. Two directories. One repository. No stashing.
cd ../hotfix-branch
# fix the bug
git commit -am "fix: resolve payment timeout on retry"
cd ../my-feature
# exactly where you left off
Clean up when done:
git worktree remove ../hotfix-branch
7. Smarter git log for Real Insights
The default git log output is verbose and hard to scan. These aliases will change your daily workflow:
# Add to your ~/.gitconfig
[alias]
lg = log --oneline --graph --decorate --all
who = log --pretty=format:"%h %an %ar - %s" --no-merges
changed = diff --stat HEAD~1 HEAD
Now:
git lg # visual branch graph, compact
git who # who committed what and when
git changed # what changed in the last commit
Or search commit messages across the whole history:
git log --all --grep="payment" --oneline
Find when a specific line was introduced (not just when the file changed):
git log -S "calculateDiscount" --oneline
This searches for commits where the string calculateDiscount was added or removed. Incredibly useful for tracking down when a function appeared or disappeared.
8. The Commit Message That Actually Helps Future You
This is not a command, it is a discipline. And it matters more than all the tricks above combined.
A bad commit message:
fix stuff
A good commit message:
fix: prevent double-charge on payment retry
The payment service was not checking for an existing pending transaction
before creating a new one. When a network timeout triggered a retry, users
could be charged twice. Added an idempotency key check before charge creation.
Closes #847
The format that works:
- First line: short summary, 50 chars or less, imperative mood ("fix", "add", "update")
- Blank line
- Body: WHY this change was made, not what (the diff already shows what)
- Reference tickets/issues
When you or your teammate is debugging at 2 AM six months from now and runs git log, that message is the difference between a clue and a dead end.
Putting It Together
You do not need to memorize all of this today. Pick one trick that solves a pain you feel right now:
- Messy history on PRs? Start with
--fixupandgit add -p. - Afraid of losing work? Learn
git reflog. - Hard to find regression bugs? Practice
git bisecton a toy project. - Context-switching costs killing you? Try
git worktree.
Git is not just version control. It is a communication tool for your team across time. The better you use it, the clearer the story of your codebase becomes — and the faster everyone can move.
Which of these was new to you? Is there a Git trick you swear by that is not on this list? Let me know in the comments.
Top comments (0)