Fixing Mistakes in Git

reset: moving the branch pointer backward

git reset moves the current branch's pointer to a different commit — what it does to the staging area and working directory depends on the flag:

graph TD soft["reset --soft <commit><br/>moves branch pointer only"] --> s1["Staging area: UNCHANGED<br/>Working dir: UNCHANGED"] mixed["reset --mixed <commit> (default)<br/>moves pointer + resets staging"] --> m1["Staging area: reset to match <commit><br/>Working dir: UNCHANGED"] hard["reset --hard <commit><br/>moves pointer + resets everything"] --> h1["Staging area: reset<br/>Working dir: OVERWRITTEN to match <commit>"]

--soft is the safest — it un-commits without touching any files, useful for "I want to redo my last commit message or split it into smaller commits" while keeping all the actual changes staged and ready. --hard is the most destructive — it discards uncommitted work in the working directory, not just the commits being reset past; this is the flag most likely to lose real work if run without checking git status first.

revert: undoing without rewriting history

graph LR c1["C1"] --> c2["C2 (bad commit)"] --> c3["C3"] --> c4["C4: revert of C2<br/>(a NEW commit)"]

git revert <commit> creates a new commit whose change is the inverse of the target commit — it doesn't remove or rewrite anything, it adds a new commit on top. This is what makes revert safe on shared/public branches where reset --hard followed by a force-push would not be: everyone else's history stays valid, they just pull one more commit like any other.

reflog: the actual safety net

Every time HEAD moves — a commit, a checkout, a reset, a rebase — Git records it in the reflog, a local, private log of where HEAD has pointed, kept even for commits that are no longer reachable from any branch.

git reflog
# a1b2c3d HEAD@{0}: reset: moving to HEAD~3
# e4f5g6h HEAD@{1}: commit: fix the thing
# ...
git reset --hard e4f5g6h   # recover the "lost" commit

After a reset --hard that turns out to have discarded commits that were actually needed, or a rebase gone wrong, the commits usually still exist — they're just no longer pointed at by any branch, so git log doesn't show them and they'll eventually be garbage collected (not immediately — Git keeps unreferenced objects for a grace period, default 90 days). The reflog is how to find the exact hash to recover before that happens.

cherry-pick: applying one commit elsewhere

git cherry-pick a1b2c3d

Takes the change introduced by one specific commit and replays it as a new commit on the current branch — useful for "this one bugfix on main also needs to land on the release branch" without merging everything else main has. Same replay mechanism as rebase (a new commit object, different hash, same resulting change), just for a single hand-picked commit instead of a whole range.

Common pitfall

Running git reset --hard to discard a bad merge or rebase, without first checking git status for uncommitted changes, can silently destroy work that was never committed at all — reset --hard doesn't distinguish "changes from commits being reset past" from "changes that were sitting uncommitted in the working directory," it overwrites the working directory to match the target commit regardless. git stash before any --hard operation is a cheap habit that turns a potentially unrecoverable mistake into a recoverable one — a stash can always be reapplied later even if the reset itself needed to happen.