105 companion flashcards · AI-assisted study content · Open the deck →
We've all been there: one wrong command and suddenly your carefully crafted commit history looks like a crime scene. This deck walks you through the most common Git slip-ups and how to recover from each one. From undoing a commit (with or without keeping the changes) to amending messages, unstaging files, rescuing deleted commits, and even force-cleaning your working directory, the cards cover practical fixes you'll reach for again and again.
The deck is aimed at developers who use Git day to day and want a reliable mental map of "oh no, how do I undo that?" If you've ever typed a command and immediately felt your stomach drop, these flashcards are for you. They're also a great refresher for anyone preparing for interviews or onboarding teammates who need to feel confident navigating commit history.
To get the most out of this deck, try practicing each command in a throwaway repository as you review. Actually running things like resets, reverts, and cherry-picks on a safe playground builds real muscle memory that flashcard recall alone can't. A spaced study schedule works especially well here, since the right command often depends on subtle differences — like whether you want to keep your changes staged or not — and revisiting the cards over a few days helps those distinctions stick.
Git offers a spectrum of "undo" commands whose power increases from safe to destructive. The lightest touch is git reset --soft HEAD~1, which moves the HEAD pointer back one commit but leaves every change staged for an immediate recommit. The default git reset --mixed HEAD~1 (or just git reset HEAD~1) does the same move but unstages the changes, leaving them in the working tree for inspection. The most destructive is git reset --hard HEAD~1, which discards both staged and working-tree changes entirely, so it should only be used when you are certain the work has no further value. If you want to revise the most recent commit rather than erase it, git commit --amend -m 'new message' rewrites the message, and git add forgotten-file followed by git commit --amend --no-edit folds in files you forgot to include without prompting for a new message.
For mistakes that never made it past the working tree, Git provides the restore family of commands (and older checkout equivalents). git restore file discards local edits to one file, while git restore . throws away every modification in the working tree. To unstage a file you accidentally added, git restore --staged file (or the legacy git reset HEAD file) moves it back to modified-but-not-staged. To temporarily inspect an older state without creating a branch, git checkout <sha> checks out a commit directly and leaves you in detached HEAD; if you decide to keep the work, git switch -c branch-name turns the detached HEAD into a real branch. These commands are safe to run repeatedly because they only touch uncommitted state.
When the wrong branch received a commit, or you simply want to move a single commit between branches, you can rescue it without losing work. From the source branch, run git log --oneline -n 3 to note the commit's SHA, switch to the destination with git switch right-branch, apply the commit via git cherry-pick <sha>, then return to the source and either git reset --hard HEAD~1 (to delete the original outright) or use rebase to drop it more surgically. The same idea works for restoring a single file from any other branch or commit using git checkout HEAD~1 -- path or git checkout branchname -- path/to/file, which writes the historical version of that path into your working tree.
For undoing a public commit that has already been pushed, git revert <sha> is the safe alternative to resetting: it creates an inverse commit that cancels the changes without rewriting shared history, leaving every collaborator's branch intact. This distinction between reset (rewinds locally, dangerous on pushed commits) and revert (adds a new commit, safe on pushed commits) is one of the most important mental models in Git.
Untracked files are handled separately from modifications, and git clean is the dedicated tool for removing them. By default it removes untracked files; adding -d extends it to untracked directories. A dry run with git clean -nd lists everything that would be deleted without touching the disk, which is the safest way to preview before running the real git clean -fd. Forgetting to clean untracked files leads to a common two-step cleanup: git restore . clears modifications in tracked files, and git clean -fd then sweeps up anything untracked that remains. Because git clean operates on files Git has never recorded, it is genuinely destructive and cannot be undone by Git itself.
Sometimes a .gitignore rule refuses to take effect because the offending file is already tracked by Git. The fix is git rm --cached path, which removes the file from the index (and from the next commit) without deleting it from your working tree; commit the removal and the ignore rule will start working. To see exactly which rule is being applied to a path, use git check-ignore -v path, and to see everything Git is currently ignoring, add --ignored to git status. Note that Git cannot track empty directories at all, so the convention is to commit a placeholder such as .gitkeep whenever a folder's presence needs to be preserved across checkouts.
Interactive rebase is Git's primary tool for cleaning up commits before sharing them. Running git rebase -i HEAD~5 opens an editor listing the last five commits; you can reorder lines to change their order, change pick to reword to edit a message, edit to pause for amendments, or squash (and fixup) to combine a commit with the previous one. The common shorthand git rebase -i HEAD~3 followed by changing every line except the first to squash collapses three commits into one. For PR-friendly workflows, git commit --fixup <sha> tags follow-up commits and git rebase -i --autosquash main automatically organizes them into the right place.
Rebases can be interrupted safely. If you change your mind mid-way, git rebase --abort returns the branch to its original tip with conflicts and all. After resolving a conflict, git add the fixed files and run git rebase --continue to apply the next commit; if you decide a commit should be dropped entirely, git rebase --skip does so. For long-running rebases, git rebase --edit-todo lets you insert break lines that pause the rebase so you can run tests before continuing.
Rebasing is also the cleanest way to incorporate upstream changes without a merge commit. On a feature branch, git rebase main replays your commits on top of the latest main; combined with the global setting git config --global pull.rebase true, even git pull will rebase instead of producing the "ugly merge commit" pattern that the default merge-based pull creates. To make a single commit from messy history, git reset --soft main collapses all your work into staged changes, and a fresh git commit -m 'clean message' produces one tidy commit.
Merging is Git's other way of combining work, and it offers several flags that change how branches are combined. The default git merge feature performs a fast-forward when possible and otherwise creates a merge commit. To require a linear history, git merge --ff-only feature aborts if a fast-forward is not possible. To always record a merge commit (useful in code review workflows where you want to see where a feature joined main), use git merge --no-ff feature. A merge in progress can always be cancelled with git merge --abort, which restores the branch and working tree to the pre-merge state.
Conflicts appear as <<<<<<<, =======, and >>>>>>> markers in the affected files. After editing to keep only the desired content and removing the markers, git add the file and continue the merge or rebase. To see which paths are still conflicted, run git status or git diff --name-only --diff-filter=U. For wholesale choices, git checkout --theirs file or git checkout --ours file accepts an entire side; note that --strategy=ours (a merge strategy, not a per-file option) keeps only our side and is rarely what you want, whereas -X theirs (an option to the recursive strategy) just biases automatic resolution toward their side.
Two conflict-related settings prevent recurring pain. The rerere feature, enabled globally with git config --global rerere.enabled true, records how you previously resolved identical conflicts and replays that resolution automatically on future merges. When you try to combine two repositories that share no common ancestor (such as an existing project into a freshly initialized folder), Git refuses with "fatal: refusing to merge unrelated histories"; the fix is to add --allow-unrelated-histories to the merge or pull, after first verifying that the histories really should be joined.
git stash provides a way to set work aside without committing it, useful when you need a clean working tree to switch branches, pull, or bisect. git stash push -m 'message' saves the current modifications onto a stack of stashes and resets the working tree. By default only tracked files are stashed; git stash push -u (or --include-untracked) folds in untracked files as well, which is handy when switching contexts to a new branch where you want a truly clean slate.
To find your way back to stashed work, git stash list shows every entry with its message and a stash reference such as stash@{0} for the most recent. git stash pop reapplies the most recent stash and removes it from the stack, while git stash apply reapplies it but keeps the entry for possible reuse; both can target a specific entry by reference, as in git stash apply stash@{2}. Removing entries without applying is done with git stash drop stash@{0} for one, and git stash clear to wipe the entire stack, which is destructive and should be used with care.
Branch housekeeping is mostly a matter of choosing the right safety flag. git branch -m new-name renames the current branch (use -M to force-overwrite an existing name). Deletion uses git branch -d branchname, which refuses if the branch has unmerged commits; git branch -D removes it anyway. To delete the matching branch on the remote, git push origin --delete branchname is the canonical command, after which git fetch -p (or --prune) on every collaborator's machine removes the stale remote-tracking references. To set the upstream tracking on first push, git push -u origin branchname; later, git rev-parse --abbrev-ref --symbolic-full-name @{u} reports which remote branch the current branch is tracking. When local and remote diverge catastrophically, git fetch origin && git reset --hard origin/branchname snaps your branch to exactly match the remote; if you only want to preview what a pull would do, git fetch && git log HEAD..@{u} --oneline lists the incoming commits without applying them.
Tags mark points in history, typically releases. git tag v1.0.0 creates a lightweight tag pointing at the current commit; git tag -a v1.0.0 -m 'release' adds an annotated tag with a message and tagger info. Tags are not pushed by default, so git push origin --tags sends them all at once. To delete, remove locally with git tag -d v1.0.0 and remotely with git push origin :refs/tags/v1.0.0.
Pushing safely matters most when force-pushing rewritten history. The naïve git push --force blindly overwrites the remote, which is why a colleague who pushed in the meantime will lose their commits. git push --force-with-lease checks first that your local view of the remote branch is still current and refuses if anyone else has added commits, making it the right default in collaborative workflows. The shorthand git push origin local-name:remote-name lets you push a local branch to a differently named remote branch, which is helpful when the names diverge by convention. To avoid the modern confusion between "checking out a branch" and "checking out a file from history", prefer git switch for branch operations; it cleanly separates branch transitions from file restoration.
When you do fall victim to an overwriting force-push, recovery is usually possible because Git keeps a journal of where HEAD and branch tips recently pointed. git reflog show branchname lists the recent tips; once you find the SHA you want, git reset --hard <old-sha> rewinds the branch to it. Crucially, do not run git pull or git fetch first, because those operations may garbage-collect the dangling commits you are about to rescue. Reflog entries expire after 90 days for reachable objects and 30 days for unreachable ones, configurable via gc.reflogExpire; once expired, dangling commits can still be located with git fsck --lost-found, after which they can be inspected with git show and recovered via cherry-pick. The related convention origin/main (with a slash) refers to a remote-tracking branch in your local repository, while origin main (with a space) names the remote and ref separately, as used in commands like git push origin main.
Git's history tools answer different questions, and choosing the right one saves time. git log --oneline --graph --decorate --all renders the branch topology in the terminal so you can see merges and divergence at a glance. For blame-style questions, git blame file annotates each line with the commit that last touched it (add -w to ignore whitespace changes), while git log -L :funcName:file traces a function's life from introduction to removal. The pickaxe form git log -S 'searchString' finds every commit whose diff adds or removes that exact string, which is invaluable for tracking when a feature was added or removed across many refactors. To see the full commit message of a single commit, run git log -1 <sha> or git show <sha>. To inspect any object stored in Git (a commit, tree, blob, or tag), git cat-file -p <sha> prints its human-readable contents.
Diffs come in many useful variants. Plain git diff shows unstaged changes, git diff --staged (or --cached) shows what is staged, and git diff --word-diff highlights changes within lines. git show --name-only <sha> lists the files a commit changed, and git diff sha1 sha2 -- file compares a single file at two points in history. Comparing branches uses the dot notation: git log main..feature lists commits in feature not yet in main, while git log feature..main does the opposite. The three-dot form git log main...feature is special: it shows the diff from the merge base of the two branches to feature, which is exactly the change set a pull request would introduce; the same logic applies with git diff main...feature. To list branches sorted by recent activity, git for-each-ref --sort=-committerdate refs/heads/ --format='%(committerdate:short) %(refname:short)' produces a clean leaderboard of recent work.
For automatically locating the commit that introduced a bug, Git ships a binary search tool called bisect. Start it with git bisect start, mark the current commit as bad with git bisect bad, mark an earlier known-good commit with git bisect good <sha>, and Git will check out intermediate commits for you to test. When a test can be expressed as a script, git bisect run ./test.sh automates the search: exit 0 means good, exit 1 means bad, and exit 125 means skip. Always finish a session with git bisect reset to return HEAD to where you started.
For mistakes that have already entered shared history, rewriting tools are needed. To remove a committed secret, use git filter-repo or the BFG Repo-Cleaner to scrub it from every commit, then force-push and rotate the secret itself. Such operations rewrite every commit's hash, so every collaborator must re-clone or carefully reset. To start over on a feature branch while keeping its name, git switch --orphan feature && git rm -rf . creates an orphan branch with no history and no files, ready for a fresh first commit. To recover a deleted branch, git reflog finds its last tip and git switch -c branchname <sha> resurrects it; the same trick, git checkout -b rescue <sha>, rescues a commit you thought was lost.
Working with forks introduces the "upstream" remote: git remote add upstream URL && git fetch upstream adds the original repository; then git switch main && git fetch upstream && git rebase upstream/main && git push origin main updates your fork's main branch with the latest upstream changes. Shallow clones speed up CI at the cost of history. git clone --depth 1 URL fetches only the most recent commit, while git clone --branch main --single-branch URL restricts the clone to one branch. If you later need full history on a shallow clone, git fetch --unshallow retrieves the rest. For monorepos where you only need a subset of folders, git sparse-checkout init --cone followed by git sparse-checkout set folder1 folder2 materializes only the specified paths after the initial clone. Submodules embed another repository at a path; the basics are git submodule add URL path, git submodule update --init --recursive, and remembering that the parent repository only tracks the submodule's pinned SHA, not its contents.
Configuration and a few quality-of-life flags tie things together. Local config lives in .git/config and is set with git config user.email 'work@example.com'; global config lives in ~/.gitconfig and is set with --global. git config --list --show-origin displays every setting and the file it comes from. Line-ending normalization trips up Windows users; setting core.autocrlf together with a .gitattributes file containing text=auto eol=lf keeps the working tree consistent. On cross-platform projects, core.ignorecase true makes file name matching case-insensitive, though this can hide real case bugs. Other useful flags include pull.rebase true to make pulls rebase by default, rerere.enabled true to remember conflict resolutions, and commit.gpgsign true with user.signingkey set to sign commits with GPG. Finally, periodic maintenance such as git gc (or git gc --aggressive --prune=now) reclaims space and keeps the object database healthy.
git reset --soft HEAD~1git reflog to find its SHA, then git checkout -b rescue <sha>git merge --ff-only featuregit branch -m new-name — add -M to force overwrite.git diff main..feature: tip-to-tip diff.git diff main...feature: diff from their common ancestor (good for PR diffs).git fetch --unshallowgit config core.ignorecase true — but be cautious on cross-platform projects.git switch main
git fetch upstream
git rebase upstream/main
git push origin maingit rebase --edit-todo to add break lines; later git rebase --continue.Drill this topic
105 flashcards on Git Recovering From Common Mistakes — free, no signup needed to start.
Study Git Recovering From Common Mistakes flashcardsLearnWiki pages are generated with AI assistance from LearnCoachAssist's reviewed study catalog and may contain errors — verify anything critical against your course materials.