3. Undoing changes¶
Intermediate · 13 min read
Git almost never loses committed work. Most "I broke everything" moments are fixed with one of the commands below, as long as you pick the right one. The deciding question is always: has anyone else got this commit yet?
flowchart TD
Q{What do you want to undo?}
Q -->|Edits not yet staged| A["git restore file"]
Q -->|Something staged by mistake| B["git restore --staged file"]
Q -->|The last commit, not pushed| C["git commit --amend<br/>or git reset"]
Q -->|Commits already pushed| D["git revert"]
Q -->|"I reset and lost commits"| E["git reflog, then git reset --hard"]
3.1 Discard edits in the working tree¶
You added a debug print and want it gone:
The second status printed nothing: app.py is back to its last committed version. This deletes uncommitted
work for good. Git never saw those edits, so it can't bring them back.
3.2 Unstage a file¶
git add . grabbed your .env with a live API key. Take it back out of the staging area before committing:
The file is untouched on disk; it's just no longer going into the next commit. Then add it to .gitignore so it
can't happen again. (If it was already committed, see page 6.)
3.3 Fix the last commit: amend¶
Wrong message, or forgot a file? If you haven't pushed yet, amend:
0a30ec9 Shorten answer
[main ab5882f] Ask for brief answers in the system prompt
Date: Thu Oct 1 10:00:00 2026 +0530
1 file changed, 1 insertion(+), 1 deletion(-)
To add a forgotten file: git add forgotten.py then git commit --amend --no-edit. Note the hash changed from
0a30ec9 to ab5882f: amend replaces the commit with a new one.
3.4 Reset: move the branch pointer back¶
git reset <commit> moves the current branch to an older commit. The three modes differ only in what happens to
the changes from the commits you "removed":
| Mode | Branch pointer | Staging area | Working tree | Use it to |
|---|---|---|---|---|
--soft |
Moves | Keeps the changes staged | Untouched | Squash the last few commits into one |
--mixed (default) |
Moves | Cleared | Keeps the changes as edits | Redo the last commits differently |
--hard |
Moves | Cleared | Discarded | Throw the commits and changes away |
Squashing two small commits into one with --soft:
git log --oneline -3
git reset --soft HEAD~2
git diff --staged --stat
git commit -m "Add retrieval settings"
8408e15 Add RERANK
5ae9a80 Add TOP_K
ab5882f Ask for brief answers in the system prompt
app.py | 2 ++
1 file changed, 2 insertions(+)
[main 7571ab6] Add retrieval settings
1 file changed, 2 insertions(+)
And throwing a commit away completely with --hard:
3.5 Reflog: the undo for your undo¶
That --hard reset was a mistake. The commit isn't in git log anymore, but Git keeps a reflog: a local diary
of everywhere HEAD has pointed.
ab5882f HEAD@{0}: reset: moving to HEAD~1
7571ab6 HEAD@{1}: commit: Add retrieval settings
ab5882f HEAD@{2}: reset: moving to HEAD~2
8408e15 HEAD@{3}: commit: Add RERANK
5ae9a80 HEAD@{4}: commit: Add TOP_K
ab5882f HEAD@{5}: commit (amend): Ask for brief answers in the system prompt
The lost commit is right there at HEAD@{1}. Jump back to it:
HEAD is now at 7571ab6 Add retrieval settings
7571ab6 Add retrieval settings
ab5882f Ask for brief answers in the system prompt
Even the two commits we squashed (5ae9a80, 8408e15) are still reachable from the reflog. Reflog entries are
kept for about 90 days by default and are local only: they're not pushed, and they can't recover edits that
were never committed.
3.6 Revert: undo a commit that's already pushed¶
Once a commit is on GitHub, others may have it. Resetting would rewrite shared history. Instead, revert adds a new commit that does the opposite:
[main 0f32945] Revert "Add retrieval settings"
Date: Thu Oct 1 10:00:00 2026 +0530
1 file changed, 2 deletions(-)
0f32945 Revert "Add retrieval settings"
7571ab6 Add retrieval settings
ab5882f Ask for brief answers in the system prompt
History stays honest (the bad commit and its reversal are both visible) and everyone can simply pull. This is how you roll back a bad deploy: revert the commit, push, and let CI deploy the reverted state.
3.7 Stash: park unfinished work¶
You're halfway through a change and need to switch branches for an urgent fix:
git stash push -m "half-done streaming flag"
git status --short
git stash list
# ...switch branch, fix, come back...
git stash pop
git status --short
Saved working directory and index state On main: half-done streaming flag
?? .env
stash@{0}: On main: half-done streaming flag
M app.py
?? .env
pop re-applies the stash and deletes it; apply re-applies and keeps it. Untracked files aren't stashed unless
you add -u. Stashes are easy to forget, so for anything longer than a quick detour, a WIP commit on a branch is
safer.
3.8 Clean: delete untracked files¶
Look at the first line. .env isn't ignored in this repo, so git clean -f would delete your API keys file
along with the junk. Always run -n first. Ignored files are left alone unless you add -x.
3.9 Which undo is safe when?¶
| Command | Rewrites history? | Safe after push? | Can lose work? |
|---|---|---|---|
git restore file |
No | Yes | Yes: uncommitted edits |
git restore --staged file |
No | Yes | No |
git commit --amend |
Yes | No | No |
git reset --soft / --mixed |
Yes | No | No |
git reset --hard |
Yes | No | Yes: uncommitted edits |
git revert |
No | Yes | No |
git stash |
No | Yes | Rarely (forgotten stashes) |
git clean -f |
No | Yes | Yes: untracked files |
If you did rewrite something you'd already pushed (amend, reset, rebase), the push will be rejected. Only force it
on your own branch, and use git push --force-with-lease, which refuses if someone else pushed in the
meantime. Never force-push main.
Interview questions¶
What's the difference between git reset and git revert?
reset moves the branch pointer back, removing commits from the branch's history (rewrites history; only for
commits nobody else has). revert creates a new commit that undoes an earlier one, so history only moves
forward; it's the safe way to undo commits that are already pushed.
Explain reset --soft, --mixed and --hard.
All three move the branch pointer. --soft keeps the undone changes staged, --mixed (the default) keeps them
as unstaged edits, and --hard discards them from the staging area and working tree. Soft is handy for
squashing commits; hard throws work away.
You ran git reset --hard and lost a commit. Can you get it back?
Yes, if it was committed. git reflog lists every position HEAD has had; find the commit there and
git reset --hard <hash> (or create a branch at it). Reflog is local and expires after about 90 days. Edits
that were never committed can't be recovered by Git.
What's the difference between --force and --force-with-lease?
--force overwrites the remote branch unconditionally, possibly destroying commits others pushed.
--force-with-lease only overwrites if the remote branch is still where you last saw it, so it refuses if
someone else pushed in the meantime. Use the lease version, and only on your own branches.
Practice¶
- Make three commits,
reset --hardback two, then recover them withreflog. - Squash your last two commits with
reset --softand compare withrebase -i. - Revert a commit from the middle of your history (not the latest) and read the result.
Next: 4. Working with GitHub Back to: Git & GitHub overview · Notes overview