Computing and the Command Line

Reset, Revert, and the Reflog

git reset moves your branch back to an earlier commit, and its three modes decide what happens to the work you undid: --soft leaves it staged, --mixed (the default) leaves it in your files, and --hard throws it away. When to revert instead, because reset rewrites history. The reflog, git's record of everywhere HEAD has been, which brings back commits that seem lost, and ORIG_HEAD for undoing the last reset. Two recipes: squashing commits with reset --soft, and moving commits made on main to a new branch.

  • 9 min
  • 8 steps
  • 3 questions
  • Lesson 37 of 80

In this lesson

  1. Three areas, one more time
  2. What reset does
  3. The three modes
  4. Reset or revert?
  5. The reflog: getting it back
  6. Two recipes
  7. Your turn
  8. So

Three areas, one more time

Module 1 introduced git’s three areas, and reset only makes sense with them in mind. Pro Git calls them three trees 1:

  • HEAD: the last commit on your current branch, and the parent of the next one.
  • The index (the staging area): your proposed next commit.
  • The working directory: the actual files you edit.

git add copies from your files to the index; git commit turns the index into a new commit and moves the branch to it. git reset runs that backward.

What reset does

git reset <commit> moves the branch you’re on so it points at <commit> 1. Commits after that point are no longer on the branch. Here’s a garden repository with four commits:

me@linuxbox:~/garden$ git log --oneline
92e275f Add squash
dbeefcd Add garlic
d832160 Add beans
e31a769 Plan the beds

HEAD~1 means “one commit before HEAD” (module 2), so git reset HEAD~1 moves main back from 92e275f to dbeefcd. The Add squash commit isn’t deleted. Nothing points at it any more, but it’s still in the repository, and you’ll see below how to get it back.

Top left, four commits: e31a Plan the beds, d832 Add beans, dbee Add garlic, 92e2 Add squash. git reset HEAD~1 moves main from 92e2 back to dbee; Add squash isn't deleted, it's just off the branch. Top right, which areas each mode resets: --soft resets the branch only and leaves the changes staged; --mixed, the default, resets the branch and the staging area and leaves the changes unstaged in your files; --hard resets the branch, the staging area, and your files, so the changes are gone, and it destroys uncommitted edits for good. Bottom, the reflog: git reflog lists d832160 HEAD@{0} reset: moving to HEAD, d832160 HEAD@{1} reset: moving to HEAD~1, dbeefcd HEAD@{2} reset: moving to HEAD~1, and 92e275f HEAD@{3} commit: Add squash, from before the resets; git reset --hard HEAD@{3} answers HEAD is now at 92e275f Add squash. The reflog is local to the repository and its entries expire after 30 to 90 days.
reset moves the branch back; the mode decides what happens to your work; the reflog undoes it. Credit: StudyCorner diagram · CC BY 4.0 · Source

The three modes

After moving the branch, reset can go on to reset the index, and then your files as well. The mode says how far it goes 1 2.

--soft moves the branch and stops. The undone commit’s changes are still in the index, staged:

me@linuxbox:~/garden$ git reset --soft HEAD~1
me@linuxbox:~/garden$ git status -s
M  beds.txt
me@linuxbox:~/garden$ git log --oneline
dbeefcd Add garlic
d832160 Add beans
e31a769 Plan the beds

The M in the first column means staged (module 1). It’s as if you’d run git add but not git commit, so you can change something and commit again.

--mixed is the default when you give no mode. It moves the branch and makes the index match it, so the changes are left in your files, unstaged 1. Going back one more commit:

me@linuxbox:~/garden$ git reset HEAD~1
Unstaged changes after reset:
M   beds.txt
me@linuxbox:~/garden$ git status -s
 M beds.txt
me@linuxbox:~/garden$ git diff
diff --git a/beds.txt b/beds.txt
index ff39cfb..99dea17 100644
--- a/beds.txt
+++ b/beds.txt
@@ -1,2 +1,4 @@
 # Garden beds
 beans
+garlic
+squash

Both undone commits’ changes are in the file, as if you’d never run git add or git commit on them.

--hard moves the branch, resets the index, and overwrites your files to match 2:

me@linuxbox:~/garden$ git reset --hard HEAD
HEAD is now at d832160 Add beans
me@linuxbox:~/garden$ git status -s
me@linuxbox:~/garden$ git log --oneline
d832160 Add beans
e31a769 Plan the beds

git reset --hard HEAD doesn’t move the branch at all; it just throws away every uncommitted change. The garlic and squash lines are gone from the file.

Pro Git singles out --hard as the one form of reset that’s dangerous, and one of very few commands where git destroys data 1. Committed work can be recovered, as you’re about to see. Uncommitted edits that --hard overwrites cannot. Before running it, git status and git diff to check what you’d lose, and commit or stash anything you might want (next lesson covers stash).

Mode Branch Index Your files The undone work ends up
--soft moved kept kept staged
--mixed (default) moved reset kept in your files, unstaged
--hard moved reset reset gone

Given a file name instead of a commit, git reset beds.txt only copies that file from HEAD into the index: it unstages it 1. That’s what older versions of git status suggested for unstaging; git restore --staged (module 2) does the same job and is clearer.

Quick check

You committed too soon. You want the commit gone but its changes kept and still staged, ready to commit again. Which command?

Reset or revert?

Both undo commits, in opposite ways:

  • git reset rewrites history: the branch moves back, and the commits drop off it.
  • git revert (module 2) adds history: a new commit that does the opposite of an old one 3.

The rule is the same as for rebasing. If a commit is only on your computer, reset freely. If you’ve pushed it and anyone else might have it, revert. Resetting a shared branch means force-pushing, and everyone else’s copy then disagrees with yours.

Quick check

A commit you pushed last week turns out to be wrong. What’s the right undo?

The reflog: getting it back

Every time HEAD moves, by a commit, a reset, a switch, a merge, or a rebase, git writes a line in the reflog, the reference log 4. After the resets above, it looks like this:

me@linuxbox:~/garden$ git reflog
d832160 HEAD@{0}: reset: moving to HEAD
d832160 HEAD@{1}: reset: moving to HEAD~1
dbeefcd HEAD@{2}: reset: moving to HEAD~1
92e275f HEAD@{3}: commit: Add squash
dbeefcd HEAD@{4}: commit: Add garlic
d832160 HEAD@{5}: commit: Add beans
e31a769 HEAD@{6}: commit (initial): Plan the beds

Newest first. HEAD@{3} means “where HEAD was three moves ago” 4, and that’s 92e275f Add squash, from before all the resets. Put main back there:

me@linuxbox:~/garden$ git reset --hard HEAD@{3}
HEAD is now at 92e275f Add squash
me@linuxbox:~/garden$ git log --oneline
92e275f Add squash
dbeefcd Add garlic
d832160 Add beans
e31a769 Plan the beds

All four commits are back. You could equally use the ID, git reset --hard 92e275f, or make a branch there with git branch rescue 92e275f to look before you leap.

For the last reset only, there’s a shortcut. Reset saves the previous tip of the branch as ORIG_HEAD 2, so the newest reset can be undone directly:

me@linuxbox:~/garden$ git reset --hard HEAD~2
HEAD is now at d832160 Add beans
me@linuxbox:~/garden$ git reset --hard ORIG_HEAD
HEAD is now at 92e275f Add squash

Two limits to know 4:

  • The reflog is local. It records moves in your repository; it isn’t pushed, and a fresh clone starts with an empty one.
  • Entries expire. By default, git’s housekeeping prunes reflog entries after 90 days, and after 30 days for commits no longer on any branch. So a commit lost to a reset has about a month.

Each branch has its own reflog too: git reflog main shows only the moves of main, and main@{one.week.ago} names where main pointed a week ago 4.

Quick check

You ran git reset --hard HEAD~2 and lost two commits you wanted. How do you get them back?

Two recipes

Squash your last few commits into one. --soft keeps the changes staged, so recommitting folds them together 2:

me@linuxbox:~/garden$ git reset --soft HEAD~2
me@linuxbox:~/garden$ git commit -m "Add garlic and squash"
[main f8509eb] Add garlic and squash
 1 file changed, 2 insertions(+)
me@linuxbox:~/garden$ git log --oneline
f8509eb Add garlic and squash
d832160 Add beans
e31a769 Plan the beds

This does the same job as squashing in git rebase -i (module 3), with less ceremony when it’s the last few commits.

Committed to main when it should have been a branch. Make a branch where you are, so the commits keep a name, then move main back. The git docs give this recipe for three commits 2:

me@linuxbox:~/garden$ git branch fence-plan
me@linuxbox:~/garden$ git reset --hard HEAD~3
me@linuxbox:~/garden$ git switch fence-plan

main is back where it was, and the three commits live on fence-plan. Do this before you push main.

Your turn

Exercises

  1. In a practice repository, make four small commits. Run git reset --soft HEAD~1 and check git status -s and git log --oneline. Then commit again.
  2. Run git reset HEAD~2 (mixed). What does git status -s show? Recommit the changes as one commit.
  3. Make an uncommitted edit, then run git reset --hard HEAD. Can you get the edit back?
  4. Run git reset --hard HEAD~3, then use git reflog to find the commit you were on and get all three back.
  5. Try ORIG_HEAD: reset back two commits with --hard, then undo it in one command.
  6. Make two commits on main that should have been on a branch, and move them to one with the recipe above.
Answers
  1. M beds.txt (staged, with M in the first column), and the log has lost its top commit. git commit -m "..." makes a new commit with a new ID.
  2. M in the second column: the changes from both commits are in your files, unstaged. git add the files and commit once.
  3. No. It was never committed, so it never reached the repository and the reflog has nothing to point to. That’s the case --hard is dangerous for.
  4. Look for the line just before the first reset: moving to HEAD~3, the most recent commit: entry, and git reset --hard to that ID or HEAD@{n}.
  5. git reset --hard ORIG_HEAD.
  6. git branch new-name, git reset --hard HEAD~2, git switch new-name. git log --oneline --graph --all shows main two commits behind the new branch.

So

git reset <commit> moves your branch back. --soft leaves the undone changes staged, --mixed (the default) leaves them in your files, and --hard throws them away, which is the one way reset loses uncommitted work. Reset only commits nobody else has; for shared ones, git revert. Committed work isn’t easily lost: git reflog lists everywhere HEAD has been, git reset --hard HEAD@{n} goes back there, and ORIG_HEAD undoes the last reset.

Lesson complete

Nice work.

1day streak
0/1today's goal
–correct

Up next · 9 min

Stash and Cherry-pick

Next lesson
Sources for this lesson
  1. 1
    Scott Chacon, Ben Straub. Pro Git, 2nd edition. Apress; free online at git-scm.com. 2014. verifiedFree CC BY-NC-SA 3.0 book, maintained online. Ch. 1: version control; Git's 2005 origin when the Linux kernel lost free use of BitKeeper; snapshots, not differences (unchanged files stored once); nearly every operation local; integrity through 40-character SHA-1 checksums; the three states (modified, staged, committed) and three areas (working tree, staging area or index, .git directory); first-time setup with system/global/local config levels, user.name and user.email baked into commits, core.editor, git config --list --show-origin. Ch. 2: git init, status (and -s), add, diff and diff --staged, commit (-m, -a), .gitignore, log options, amending, undoing, remotes, tags, aliases. Ch. 3: branches as movable pointers, HEAD, merging and conflicts, remote branches, rebasing and its rule. Ch. 7: reset demystified, stashing, revision selection. Ch. 8: core.autocrlf true on Windows, input on Linux and macOS. Ch. 10: objects (blob, tree, commit) and references.
  2. 2
    git-reset documentation. Git project (git-scm.com). verifiedSets the current branch head to a commit; --soft leaves the index and working tree alone, --mixed (the default) resets the index but not the working tree, --hard also overwrites the working tree. Sets ORIG_HEAD to the previous tip. Examples: git reset --soft HEAD~5 then commit to combine commits; git branch topic/wip, git reset --hard HEAD~3, git switch topic/wip to move commits to a branch.
  3. 3
    git-revert documentation. Git project (git-scm.com). verifiedGiven existing commits, revert the changes they introduce by recording new commits; requires a clean working tree; --no-edit skips the message editor. Distinct from reset (which moves a branch) and restore (which changes files).
  4. 4
    git-reflog documentation. Git project (git-scm.com). verifiedReflogs record when branch tips and other references were updated in the local repository; HEAD@{2} means where HEAD was two moves ago, master@{one.week.ago} where master pointed a week ago. Expiry defaults: gc.reflogExpire 90 days, gc.reflogExpireUnreachable 30 days for entries not reachable from the current tip.