The command that has saved me the most times isn’t in most cheat sheets: git reflog. Someone runs git reset --hard on the wrong branch, watches an afternoon of work disappear from the working tree, and assumes it’s gone for good. It almost never is. Git kept a record of where that branch pointed a minute ago, and reflog will show it to you.
Most git confusion clears up the moment you stop thinking in files and start thinking in pointers.
What git is actually storing
A commit is a full snapshot of your tree, not a diff, identified by a hash of its contents. A branch is a movable pointer to one commit. HEAD is a pointer to the branch you have checked out. That’s close to the whole data model. Once branches read as cheap labels and commits read as permanent objects, most operations stop feeling dangerous. You are rarely deleting work. You are moving a label from one commit to another, and the old commit is still sitting in the object database.
The reflog is what makes that concrete. Every time HEAD or a branch moves, git appends a line to the reflog recording the old position. Those entries stick around for about 90 days for reachable commits and 30 days for ones nothing points to, controlled by gc.reflogExpire. A “lost” commit inside that window is a lookup, not a disaster.
The three places a change can live
Edit a file and it changes in the working tree. Run git add and git copies that version into the staging area, also called the index. Run git commit and git turns the index into a new commit. This is why git status can list the same file under both “Changes to be committed” and “Changes not staged for commit” at once: you staged it, then edited it again, so the index holds one version and the working tree holds a newer one. Getting asked to explain that in a pairing round is common, and it’s the answer people most often fumble.
The commands worth keeping close
This is the set I actually reach for in a normal week, with the traps that bite people who copy a command without reading it.
| What you want to do | Command | Watch out for |
|---|---|---|
| See what’s staged versus not | git status -s, git diff, git diff --staged |
Plain diff hides staged changes; add --staged to see what a commit would include |
| Stage part of a file | git add -p |
Splits a sprawling change into clean, reviewable commits |
| Undo the last commit, keep the changes | git reset --soft HEAD~1 |
Moves the branch back and leaves the work staged; --hard would delete it |
| Unstage a file | git restore --staged file |
Keeps your edits; only removes it from the index |
| Discard edits to a file | git restore file |
Destroys uncommitted changes to that file with no reflog to recover them |
| Undo a commit that’s already pushed | git revert a1b2c3d |
Adds a new inverse commit; safe on branches other people share |
| Recover a commit after a bad reset | git reflog, then git switch -c save HEAD@{2} |
Reflog entries last about 90 days |
| Replay your branch on the latest main | git rebase main |
Don’t rebase a branch other people have already pulled |
| Push after rewriting local history | git push --force-with-lease |
Refuses the push if someone else pushed since your last fetch |
| Clean up recent commits | git rebase -i HEAD~4 |
Mark lines reword, squash, or fixup in the editor |
| Copy one commit to your branch | git cherry-pick a1b2c3d |
Handy for hotfixes; can duplicate the change if you later merge the source |
| Shelve work in progress | git stash push -m "wip", git stash pop |
Pop can conflict, and forgotten stashes pile up silently |
| Work two branches at once | git worktree add ../hotfix main |
A second working directory on the same repo, no stashing needed |
| Find the commit that broke a test | git bisect start, git bisect good, git bisect bad |
Binary search across history; automate it with git bisect run |
| See who last changed a line | git blame -L 10,20 file |
Pair with git log -p to read the reasoning behind the change |
reset, revert, and restore do three different jobs
These three get mixed up more than anything else in git, and interviewers like the question because the wrong choice on a shared branch causes real damage.
git reset moves your current branch pointer to a different commit. With --soft the changes from the removed commits stay staged, with --mixed (the default) they stay in the working tree unstaged, and with --hard they are wiped from the working tree. Reset rewrites where your branch points, so keep it for history that only lives on your machine.
git revert never rewrites anything. It creates a new commit that undoes an earlier one, which makes it the right tool for a commit you already pushed and others have pulled.
git restore is the newer, narrower command for files. git restore file throws away working-tree edits, and git restore --staged file unstages a file without touching your edits. It took over the file-level jobs that git checkout and git reset used to overload, which is why older answers online still use checkout for this.
Getting work back after a bad command
Here is the move that recovers a hard reset. Say you meant to drop one commit and dropped three:
$ git reset --hard HEAD~3 # meant to undo one, undid three
$ git reflog
a1b2c3d HEAD@{0}: reset: moving to HEAD~3
9f8e7d6 HEAD@{1}: commit: add retry to the upload path
1122eef HEAD@{2}: commit: fix flaky timeout test
$ git switch -c recovered 9f8e7d6
The reflog lists every position HEAD held, newest first. Find the line from just before the mistake, then point a fresh branch at that hash. The same trick undoes a botched rebase or a bad merge: reflog back to the state before it, and branch off that hash. The only work reflog cannot bring back is edits you never committed or staged, because those were never written to an object. That is the real argument for committing early and often, even when the messages are rough.
Rewriting history you haven’t shared
Interactive rebase is how you turn a messy local branch into something reviewable. git rebase -i HEAD~5 opens the last five commits in an editor where you mark lines to reword, squash, or drop. The cleaner path for fixups is to commit against a specific earlier commit and let git slot it into place:
$ git commit --fixup a1b2c3d
$ git rebase -i --autosquash HEAD~6
The rule that keeps this safe: rebase and amend only on commits nobody else has based work on. When you do rewrite a branch you already pushed, push with git push --force-with-lease instead of plain --force. Force-with-lease refuses the push if someone else added commits since you last fetched, so you overwrite your own outdated view rather than their work.
stash, worktree, and the interrupted task
git stash shelves half-finished work so you can switch context, and git stash pop brings it back. The catch is that pop can conflict, and a stash you forget about stays invisible until you run git stash list. For anything longer than a quick interruption, git worktree add ../hotfix main is the better answer. It checks out a second branch into its own directory backed by the same repo, so you can build and test an urgent fix without stashing or disturbing the branch you were on. When someone asks how you would handle a production emergency mid-feature, worktree is a sharper answer than “stash everything and hope nothing breaks.”
The engineer who stays calm during a git mess isn’t smarter about git. They just know the object database keeps almost everything, and that git reflog is the first place to look before assuming any commit is gone.
Practice the behavioral round:
