Skip to content

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:

git status --short
git restore app.py
git status --short
Output
 M app.py

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:

git add .
git status --short
git restore --staged .env
git status --short
Output
A  .env
?? .env

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:

git log --oneline -1
git commit --amend -m "Ask for brief answers in the system prompt"
Output
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"
Output
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:

git reset --hard HEAD~1
Output
HEAD is now at ab5882f Ask for brief answers in the system prompt

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.

git reflog -6
Output
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:

git reset --hard HEAD@{1}
git log --oneline -2
Output
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:

git revert --no-edit HEAD
git log --oneline -3
Output
[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
Output
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

git clean -n          # dry run: show what WOULD be deleted
Output
Would remove .env
Would remove notes.tmp
Would remove scratch.ipynb

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 --hard back two, then recover them with reflog.
  • Squash your last two commits with reset --soft and compare with rebase -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