Undoing Mistakes: Branching & Merging — The Complete Guide
Why This Matters
Every engineer has that moment. You push a broken commit to main, accidentally delete a branch, or merge something that wasn't ready. Your heart drops. You open Stack Overflow with trembling fingers.
The good news: Git is designed to let you recover from almost anything. The bad news: you have to understand how branching and merging actually work to wield that power. This guide walks you through both — from the fundamentals to the "oh no" recovery playbook.
Branching Fundamentals
A branch in Git is simply a lightweight, movable pointer to a commit. When you create a branch, Git doesn't copy your codebase — it just creates a new pointer. This is why branching in Git is nearly instantaneous, regardless of project size.
Creating and Switching Branches
# Create and switch in one command
git checkout -b feature/auth-flow
# Modern alternative (Git 2.23+)
git switch -c feature/auth-flow
# List all branches
git branch -a
# Delete a branch (safe — won't delete unmerged work)
git branch -d feature/auth-flow
# Force delete (use with caution)
git branch -D feature/auth-flow
feature/, fix/, hotfix/, chore/. Your future self (and your team) will thank you during a 2 AM incident.
Merging Strategies
Merging brings divergent histories back together. Git offers several strategies, and choosing the right one depends on your team's workflow and how clean you want your history.
Fast-Forward Merge
When the target branch hasn't diverged, Git simply moves the pointer forward. No merge commit is created — the history stays perfectly linear.
git checkout main
git merge feature/auth-flow
# If main hasn't moved, this is a fast-forward
Three-Way Merge
When both branches have new commits, Git performs a three-way merge using the common ancestor, creating a merge commit. This preserves the full branching history.
git checkout main
git merge --no-ff feature/auth-flow
# --no-ff forces a merge commit even if fast-forward is possible
Rebase (Linear History)
Rebasing replays your branch's commits on top of the target branch, resulting in a clean, linear history. It's powerful — but it rewrites commit hashes.
git checkout feature/auth-flow
git rebase main
# Then fast-forward main
git checkout main
git merge feature/auth-flow
Undoing Mistakes
This is the section you'll bookmark. Here's your recovery toolkit, ordered from least destructive to most destructive.
1. Amend the Last Commit
Forgot a file? Typo in the commit message? Amend it before pushing.
git add forgotten-file.ts
git commit --amend
# Opens editor to update the message. Use --no-edit to keep it.
2. Undo Staged Changes
# Unstage a file (keep changes in working directory)
git restore --staged src/app.ts
# Discard working directory changes entirely
git restore src/app.ts
3. Revert a Commit (Safe for Shared Branches)
git revert creates a new commit that undoes the changes of a previous one. It doesn't rewrite history, making it safe for main.
git revert abc123
# Creates a new commit that undoes abc123
# Revert a merge commit (specify which parent to keep)
git revert -m 1 merge-commit-hash
4. Reset (Rewrite Local History)
Reset moves the branch pointer backward. Three modes give you different levels of control:
# Soft: undo commit, keep changes staged
git reset --soft HEAD~1
# Mixed (default): undo commit, unstage changes
git reset HEAD~1
# Hard: undo commit AND discard all changes
git reset --hard HEAD~1
git reset --hard permanently discards uncommitted work. Always double-check with git status and git stash before using it.
5. Reflog — Your Ultimate Safety Net
Even after a hard reset, Git keeps a log of every HEAD position for 90 days. The reflog is how you recover "lost" commits.
git reflog
# Find the commit hash before your mistake, then:
git reset --hard HEAD@{3}
# Or cherry-pick a specific lost commit
git cherry-pick abc123
git reflog is the command that turns panic into relief.
Resolving Merge Conflicts
Conflicts happen when two branches modify the same lines. Git can't decide which version wins, so it marks the file and asks you to resolve it manually.
<<<<<<< HEAD
const timeout = 3000;
=======
const timeout = 5000;
>>>>>>> feature/auth-flow
To resolve: edit the file to keep the correct code, remove the conflict markers, then stage and commit.
# After manually resolving conflicts:
git add src/config.ts
git commit
# If you want to abort the merge entirely:
git merge --abort
For complex conflicts, tools like git mergetool, VS Code's built-in merge editor, or dedicated tools like kdiff3 can save significant time.
Real-World Workflows
Understanding the commands is half the battle. The other half is knowing when to use them within your team's workflow. Here's a practical pattern I've used across multiple teams:
Feature development: Branch off main, keep your branch short-lived (ideally a few days, not weeks), rebase onto main before opening a PR to keep your diff clean, and squash-merge into main when approved.
Hotfixes: Branch off the release tag or main, fix, test, merge, and backport if needed. Use git cherry-pick to apply the fix across multiple release branches.
Recovering from a bad merge to main: Use git revert, never git reset --hard on a shared branch. Communicate with your team, push the revert, then investigate at your own pace.
Final Thoughts
Git's power comes from its flexibility — but that same flexibility can be intimidating. The key takeaways that have saved me countless times:
Commit often. Small, frequent commits give you more granular recovery points. Use git stash liberally before risky operations. Know the difference between revert and reset — one is safe for shared history, the other isn't. And above all, remember that git reflog exists. It's your parachute.
Mistakes aren't failures — they're part of the development process. The engineers who ship with confidence aren't the ones who never make mistakes. They're the ones who know how to undo them.
Happy branching.
© 2026 · Written with too many cups of coffee and hard-earned git scars.