Menu for Per-Arne's Git Good!
Courses and project Per-Arne's Git Good!

Move the branch back: git reset

Module 4: Undo and rewrite · 12 min

The interactive lesson needs JavaScript. The text below is the complete lesson.

After this lesson you can

  • undo local commits by moving the branch with git reset, choosing the mode (--soft, --mixed, --hard) from what should happen to staging and the working files apply
  • set a backup branch before a risky reset, and reset back to it afterwards apply
  • decide when reset is safe, and when restore or revert fits better evaluate

Understand: One pointer, three reaches

You committed B a little too early. Nobody else has seen it. Right now the version with Draft is in all three places: in the working tree, in staging and in the last commit.

git reset --soft HEAD~1 moves only the branch. main jumps back to A. Staging and the working file are untouched, so the change now looks staged and ready for a new commit.

Back at B: git reset HEAD~1 with no mode means --mixed. It moves the branch and also copies A into staging. The working file keeps Draft, now as an unstaged change.

Again from B: git reset --hard HEAD~1 reaches all the way to the working tree. All three places get the version from A. This is the dangerous mode: uncommitted edits in tracked files are overwritten without asking.

Look at B. It is drawn dashed, because no branch points to it any more. But it is not deleted. If you know its ID, git reset --hard moves main back to it. The lesson “Reflog” shows how to find the ID.

Here is the rule. Reset moves your branch, and the mode decides how far to the left it reaches. Use reset only on commits nobody else has. If it concerns only files or staging, restore is enough. If the commit is shared, use revert.

Watch: Three modes on the same commit

We have two commits, and we want to redo the last one. We start with the mildest mode.

$ git reset --soft HEAD~1

We commit again to try the next mode.

$ git commit -m "Add draft"

Now with no mode, which means --mixed. Watch the staging card: it goes back to Welcome, while the working file still has Draft.

$ git reset HEAD~1

We stage and commit one last time.

$ git commit -am "Add draft"

Now the hard one. Watch all three cards go back to Welcome. The Draft line is gone from the file, but the commit still exists.

$ git reset --hard HEAD~1

Tasks

  1. Undo the last commit, but keep its change staged.
  2. Undo the last commit and unstage its change, but keep it in the file.
  3. Remove the last commit completely from the branch, staging and the working file.

Practise: Transfer: tidy up a login page

None of the commits are pushed. Look at git log --oneline before you start.

Tasks

  1. Add login form and Add login button are really one change. Combine them into one commit with the message Add login page.
  2. You want to experiment with a hard reset. First set a safety bookmark: a branch backup on the commit you are on, without switching to it. Then remove Add login page completely from main, staging and the files.
  3. The experiment was a mistake. Bring main back to your bookmark, without searching the reflog.

Check yourself: Recall it

Your two latest local commits are really one change. You want to turn them into one commit and keep all the changes staged. Which command starts the job?

  • git reset --soft HEAD~2
  • git reset --hard HEAD~2
  • git restore --staged .

What can git reset --hard destroy for good?

  • Commits you have made
  • Uncommitted edits in tracked files
  • Every untracked file in the folder

An hour ago you pushed feature, just as a backup. Nobody else uses the branch. Now you want to combine its last two commits into one. What do you do?

  • git reset --soft HEAD~2, git commit, then git push
  • git reset --soft HEAD~2, git commit, then git push --force-with-lease
  • git revert on the two commits, then git push

Review from “Reading history”: what does HEAD~2 point to in git reset --hard HEAD~2?

  • The commit two steps back along the first parent, that is HEAD's grandparent
  • The second parent of HEAD
  • Where HEAD was two commands ago

Common misconceptions

  • “Reset deletes commits.” Reset moves a branch. Commits left behind still exist for quite a while.
  • “Every reset mode changes my files.” Of --soft, --mixed and --hard, only --hard touches the working files. --soft and --mixed leave them alone.
  • “Reset is fine for commits I have pushed too.” The team still has the old commits. Pushed history is undone with revert.

Read more

What reset does, step by step

git reset <commit> works in up to three steps, and the mode decides where it stops:

  1. Move the branch HEAD points to, for example main, to <commit>. HEAD follows, because it points to the branch.
  2. Make the staging area, also called the index, match <commit>. Skipped with --soft.
  3. Make the tracked files in the working tree match <commit>. Only with --hard.
Mode Branch and HEAD Staging area Working tree
git reset --soft HEAD~1 moved unchanged unchanged
git reset HEAD~1 (same as --mixed) moved = HEAD~1 unchanged
git reset --hard HEAD~1 moved = HEAD~1 = HEAD~1

Git tells you what happened. After a mixed reset it lists the files whose changes are now unstaged; after a hard reset it names the commit you landed on:

$ git reset HEAD~1
Unstaged changes after reset:
M   README.md
$ git reset --hard HEAD~1
HEAD is now at 831f7ba Initial snapshot

git reset with no commit means git reset HEAD: the branch stays put, and the staging area is reset to the last commit. In practice that unstages everything and keeps all edits in the files.

Reset with paths is a different command

git reset -- README.md or git reset HEAD~1 -- README.md never moves a branch. It copies the file from the commit (default HEAD) into the staging area only, which is what git restore --staged does. The working file is untouched:

$ git reset -q HEAD~1 -- README.md
$ git status --short
MM README.md

MM means the staging area now holds the older version (a staged change compared with HEAD), while the working file still has the newest one. Because the branch does not move, the modes make no sense with paths:

$ git reset --soft HEAD~1 -- README.md
fatal: Cannot do soft reset with paths.

git reset -p lets you pick hunks to unstage, like git restore -p --staged.

Exactly what --hard destroys

--hard overwrites uncommitted changes in tracked files, both staged and unstaged. Untracked files survive, unless they are in the way of a tracked file in the target commit: an untracked file with the same name, or an untracked folder where the commit has a file, is deleted without warning. Committed work is not destroyed, because the old commit still exists and the reflog remembers it. Before an experiment, two cheap safety nets help:

  • git stash -u puts uncommitted changes, untracked files included, aside, so a hard reset cannot touch them.
  • git branch backup puts a bookmark on the current commit. Afterwards git reset --hard backup takes you straight back, without searching in the reflog.

Gentler alternatives: --keep and --merge

git reset --keep HEAD~1 moves the branch like --hard, but keeps your local edits, and refuses if an edited file would have to be overwritten:

$ git reset --keep HEAD~1
error: Entry 'notes.txt' not uptodate. Cannot merge.
fatal: Could not reset index file to revision 'HEAD~1'.

Nothing changed, so you can commit or stash first. That makes --keep a safer habit than --hard when you only want to drop commits. --merge is meant for leaving a merge with conflicts; today git merge --abort does that job and is easier to remember.

Getting back after a reset

Reset stores the previous position in ORIG_HEAD, so right after a mistaken reset git reset --hard ORIG_HEAD takes you back. If you have uncommitted work you want to keep, use --keep instead of --hard. Older positions are in the reflog, which the lesson “Reflog” shows.

Never reset what others have

Reset rewrites your branch. If the commits you drop are already pushed, git push is rejected because your branch no longer contains the server's commits. Forcing the push through deletes those commits from the server, including any work teammates pushed on top. For shared history, use git revert instead. The table in the lesson “Revert” shows which tool fits when.

Official documentation