Git basics · Lesson 3 of 6 · about 25 minutes

Undoing things

Throw away a change, unstage a file, fix your last commit, undo a commit safely, and get back work you thought you'd lost. Plus the one rule that keeps undoing from causing trouble for everyone else.

By the end you'll know

  • What HEAD is, and how to point at earlier commits
  • Which command undoes which kind of mistake
  • The difference between undoing safely and rewriting history, and when each is fine
  • How to get back commits you thought you'd lost

Where you are: HEAD

Every undo command needs to know where you're starting from. Git keeps a marker for that, called HEAD: it points at the commit you're on right now, which is usually the latest commit on your branch. In your git-practice folder from Lesson 2:

BASH
git log --oneline
TEXT
9b7b6db (HEAD -> main) Update the to-do list
2308b1d Stop tracking log files
31077a4 Add debug notes

(Your commit IDs will be different.) (HEAD -> main) means "you are here: on the branch main, at this commit". Lesson 4 explains branches; for now, think of HEAD as the "you are here" pin on a map.

You can count back from it. HEAD~1 means "one commit before HEAD", HEAD~2 means two before, and so on. Try it:

BASH
git show --stat HEAD~1

That shows the commit before your latest one, "Stop tracking log files", and which files it changed.

Which undo do you need?

The right command depends on one question more than any other: has this change been committed, and has anyone else seen that commit?

Choosing the right undoIf the change isn't committed yet: to throw away a change in a file, use git restore; to take a file out of the staging area but keep the change, use git restore --staged. If it's committed but not shared with anyone yet: to fix the last commit's message or add a forgotten file, use git commit --amend; to undo the last commit but keep its changes, use git reset HEAD~1. If it's committed and already shared: use git revert, which adds a new commit that undoes it.Not committed yetonly in your folder or stagedThrow away a change:git restore filegone for good, not in historyUnstage, keep the change:git restore --staged filesafeCommitted, not sharedonly on your laptopFix the last commit:git commit --amendreplaces it with a new oneUndo it, keep the changes:git reset HEAD~1rewrites your historyCommitted and sharedpushed, others may have itUndo it with a new commit:git revert commithistory is added to,never rewrittenRewrite only what nobody else has seen. Once it's shared, revert.
Before committing, restore. After committing but before sharing, amend or reset. Once a commit is shared, revert it with a new commit instead of rewriting history.

Work through each one in your git-practice folder.

Before committing

Throw away a change you haven't committed. You edited the wrong file:

BASH
echo "oops, typed in the wrong file" >> README.md
git status --short
TEXT
 M README.md

Put the file back the way it was in your last commit:

BASH
git restore README.md
git status --short

Nothing listed: the change is gone. Be careful with this one. The change was never committed, so Git has no copy of it: git restore on a file throws your edits away for good.

Unstage a file, but keep the change. You added a file to the staging area that shouldn't go in this commit yet:

BASH
echo "- learn git restore" >> todo.txt
git add todo.txt
git status --short
TEXT
M  todo.txt

The M in the first column means "staged". Take it back out of the box:

BASH
git restore --staged todo.txt
git status --short
TEXT
 M todo.txt

The M moved to the second column: still changed in your folder, just not staged. Nothing was lost. This is completely safe, and it's what git status itself suggests when you've staged something.

After committing, before sharing

Fix your last commit. You committed, then spotted a typo in the message:

BASH
git add todo.txt
git commit -m "Add git restre to the list"

--amend replaces the last commit with a corrected one:

BASH
git commit --amend -m "Add git restore to the to-do list"
git log --oneline -2
TEXT
4fe464e Add git restore to the to-do list
9b7b6db Update the to-do list

The typo is gone. If you'd forgotten a file instead, git add it first, then git commit --amend --no-edit (keep the same message). Notice the commit ID changed: amending doesn't edit the old commit, it makes a new one and throws the old one away. That detail matters in a moment.

Undo the last commit, but keep its changes. You committed too early:

BASH
echo "- learn git reset" >> todo.txt
git commit -am "Add git reset to the list"
git reset HEAD~1
TEXT
Unstaged changes after reset:
M	todo.txt

(-a in git commit -am stages every tracked file that changed, and commits it, in one step.) git reset HEAD~1 moved your branch back one commit, so the commit is gone from the history, but its changes are still in your folder, ready to be committed again properly. Check with git log --oneline -1 and git status. Then throw the change away, since it was only practice:

BASH
git restore todo.txt

The rule: don't rewrite shared history

--amend and reset rewrite history: they remove commits and put different ones in their place. On your own laptop, before anyone else has seen those commits, that's fine; it's tidying up.

Once a commit has been pushed and someone else has it, rewriting it causes real trouble. Their copy still has the old commits, yours has new ones, and Git sees two histories that don't agree. So the rule is simple: once a commit is shared, don't rewrite it. Revert it.

Think of a bookkeeper's ledger. While you're still drafting an entry in pencil, you can erase it. But once an entry is in the ledger and others have copied the numbers, you never erase it. You add a new line: "correction: reverses the entry above". The mistake and its fix are both on record, and everyone's copy still agrees. That's git revert.

Undo a shared commit: revert

Make a commit you'll regret:

BASH
echo "This line is a mistake." >> README.md
git commit -am "Add a line to the README"

Imagine it's already pushed. Revert it:

BASH
git revert --no-edit HEAD

--no-edit accepts Git's suggested message, Revert "Add a line to the README". Without it, Git opens a text editor so you can change the message first. On a Mac that's often vim, which surprises people: if you end up in it, type :wq and press Enter to save and close. Then:

BASH
git log --oneline -3
TEXT
2d59507 Revert "Add a line to the README"
db4b1f8 Add a line to the README
4fe464e Add git restore to the to-do list

The mistake is still in the history, followed by a new commit that undoes it. Check the file with tail -1 README.md, which shows a file's last line: the bad line is gone. Nothing was rewritten, so anyone who already has your commits just gets one more. To revert an older commit, give its ID instead of HEAD: git revert db4b1f8.

In my real setup

Revert is how I roll back production. My CivicGrid cluster runs whatever my GitOps repository says, so shipping a new version of the API is one commit that changes the image tag. The comment above that line in the file says it plainly: rollback = revert this commit. When I shipped a change to which API keys users could create themselves, my notes for it included the exact rollback command, git revert 6d809e6, written down before I needed it.

I never use reset on main for this. The commit is already on GitHub, Argo CD has already acted on it, and the history is my record of what ran in production and when. A revert keeps that record honest: the change, and the decision to undo it, are both there.

Break it on purpose

Lose commits, then get them back. git reset --hard is the dangerous version of reset: it moves your branch back and throws away the changes in your folder. Throw away your last two commits:

BASH
git reset --hard HEAD~2
git log --oneline -2
TEXT
4fe464e Add git restore to the to-do list
9b7b6db Update the to-do list

The revert and the commit before it are gone from the log. They look lost, but they aren't. Git keeps a private diary of everywhere HEAD has been, called the reflog:

BASH
git reflog -3
TEXT
4fe464e HEAD@{0}: reset: moving to HEAD~2
2d59507 HEAD@{1}: revert: Revert "Add a line to the README"
db4b1f8 HEAD@{2}: commit: Add a line to the README

HEAD@{1} is where HEAD was one move ago: right before the reset. Go back there:

BASH
git reset --hard 'HEAD@{1}'
git log --oneline -3

Both commits are back. The quotes around HEAD@{1} keep some shells from misreading the curly brackets.

The reflog is your safety net for committed work: it keeps every commit HEAD has visited for weeks, even after a reset. It can't help with changes you never committed, though. That's why git restore and git reset --hard deserve a pause before you press Enter: if the work wasn't committed, it's gone. When in doubt, commit first. You can always undo a commit; you can't undo an unsaved change.

Clean up

Nothing to remove. Keep git-practice for Lesson 4.

Check yourself

  1. What does HEAD point to?

  2. You staged a file by mistake and want it out of the next commit, without losing your edits. What do you run?

  3. You just committed, haven't pushed, and the message has a typo. What's the simplest fix?

  4. A bad commit is already pushed, and others have pulled it. How should you undo it?

  5. You ran git reset --hard HEAD~2 and lost two commits. Can you get them back?

  6. Which kind of work can't the reflog recover?

Key terms

  • HEAD: the commit you're on right now. HEAD~1 is the one before it.
  • git restore: throws away uncommitted changes in a file. --staged only unstages, keeping the change.
  • git commit --amend: replaces the last commit with a corrected one.
  • git reset HEAD~1: moves your branch back one commit, keeping its changes in your folder. --hard throws them away too.
  • Rewriting history: removing or replacing commits. Fine before sharing, trouble after.
  • git revert: adds a new commit that undoes an earlier one. The safe way to undo shared commits.
  • reflog: Git's record of everywhere HEAD has been, for recovering commits.

Go deeper