first, know which of the three states you're undoing

Every change to a tracked file passes through up to three places before it's part of history: the working tree (what's on disk), the index (what's staged with git add), and history (what's committed). Which command you need depends entirely on which of these three still holds the mistake.

undoing uncommitted changes

git restore <file> | https://git-scm.com/docs/git-restore | discards uncommitted changes to a file, back to the last commit |'gu_wt1'
git checkout -- <file> | | the older, pre-2.23 equivalent of the command above |'gu_wt2'
git clean -fd | https://git-scm.com/docs/git-clean | deletes untracked files and directories — nothing to restore, so this one is permanent |'gu_wt3'
Run git clean -n first (dry run) — it lists exactly what would be deleted without deleting it.

undoing staged changes

git restore --staged <file> | https://git-scm.com/docs/git-restore | unstages a file, keeping the changes in your working tree |'gu_idx1'
git reset HEAD -- <file> | | the older equivalent of the command above |'gu_idx2'

fixing the last commit

git commit --amend | https://git-scm.com/docs/git-commit | rewrites the last commit — new message, or add whatever's currently staged to it |'gu_amend1'
git commit --amend --no-edit | | same, but keeps the existing commit message unchanged |'gu_amend2'
Amending creates a brand-new commit with a new hash — it doesn't edit the old one in place. Never amend a commit you've already pushed and that someone else might have based work on.

reset: soft, mixed, hard

git reset moves a branch pointer backward. The three flags control how much it also touches your index and working tree — this is the single most misused command in git, so know the table before you run it.
FlagMoves branchResets indexResets working tree
--softyesno — changes land back as stagedno
--mixed (default)yesyes — changes land back as unstagedno
--hardyesyesyes — uncommitted work is gone
git reset --soft HEAD~1 | | undoes the last commit, keeps everything staged, ready to re-commit |'gu_reset1'
git reset --hard HEAD~1 | | undoes the last commit AND discards its changes entirely |'gu_reset2'

reset vs. revert

reset rewrites history — it moves what a branch points to, so anyone who already pulled the old commits now disagrees with you. revert adds a new commit that undoes an old one's changes, leaving history intact. Once something is pushed and shared, revert is the safe choice.
git revert <commit> | https://git-scm.com/docs/git-revert | creates a new commit that undoes the changes from <commit> |'gu_revert1'
git revert -n <commit> | | same, but stages the undo without committing, so you can combine several |'gu_revert2'

the reflog: git's safety net

Every time HEAD moves — commits, resets, rebases, checkouts, merges — git logs it locally in the reflog, kept for 90 days by default. This is what makes almost every "mistake" in git recoverable: as covered in Git Internals, nothing is actually deleted until garbage collection runs and nothing points at it anymore.
git reflog | https://git-scm.com/docs/git-reflog | lists every recent position of HEAD, newest first, each with a short hash |'gu_reflog1'
git reset --hard HEAD@{2} | | jumps HEAD (and the branch) back to wherever it was 2 moves ago in the reflog |'gu_reflog2'
git checkout <hash-from-reflog> | | inspects a lost commit directly before deciding what to do with it |'gu_reflog3'

recovering a deleted branch

Deleting a branch only deletes the pointer — the commits themselves stick around until gc runs. The reflog remembers where the branch tip was right up until the delete.
git reflog | grep <branch-name> finds the last known commit hash for a branch you just deleted
git branch &lt;branch-name&gt; &lt;hash-from-reflog&gt; | | recreates the branch pointing at that commit |'gu_del2'

escaping a bad merge or rebase

git merge --abort | https://git-scm.com/docs/git-merge | bails out of a merge with conflicts, back to exactly how things were before it started |'gu_bad1'
git rebase --abort | https://git-scm.com/docs/git-rebase | same idea, mid-rebase |'gu_bad2'
Already finished a rebase or merge and regretted it? The reflog has you covered here too — git reflog to find HEAD's position right before it started, then git reset --hard to that hash.

grabbing one good commit off a bad branch

git cherry-pick &lt;commit&gt; | https://git-scm.com/docs/git-cherry-pick | applies one specific commit's changes onto your current branch |'gu_cherry1'
Useful when a branch went sideways but one commit on it is still worth keeping — cherry-pick that one commit onto a clean branch instead of untangling the whole thing.

related topics

Git Internals — why the reflog can recover things: refs are just files, objects aren't deleted until gc runs.
Git Cheat Sheet — the everyday commands this page assumes you already know.

reference

git-scm.com — git reflog
git-scm.com — git reset
Pro Git — Reset Demystified