Git and GitHubgit 2.54 · GitHub · macOS, Linux or Windows
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
26 small wins to finish your pathNext lesson →

Undoing changes before a commit

git restore throws away a change you have not committed, either the edit in the file or the fact that you staged it.

Last updated: 12 Oct, 2026 · git 2.54

Forks finished everything about sharing work. This part is about taking work back, and it starts with the cheapest case: the change is not committed yet, so undoing it touches no history at all.

Where a change sits and what takes it back. A change starts in the working tree, the file on disk; git add moves it to the staging area; git commit moves it into the last commit. Red arrows run the other way. git restore of a file throws the edit away and copies the staged version back over it. git restore --staged of a file takes it out of the staging area and leaves the file on disk untouched. git reset --soft HEAD~1 undoes the commit and leaves its changes staged. An untracked file is in none of the three places and git restore does not know it: git clean -n lists what would go and git clean -fd removes it.

The command you need depends only on how far the change has travelled. Deciding that first makes the rest obvious: a change you have edited but not staged, a change you have staged but not committed, and a file git has never seen are three different situations with three different answers.

Syntax: the three forms of git restore, and the one for untracked files.

bash
git restore settings.py              # throw away the edit, take the staged version back
git restore --staged settings.py     # unstage it, keep the edit on disk
git restore --staged --worktree settings.py   # both: back to the last commit
git clean -n                         # list the untracked files that -f would delete

Setting up a repository to practise in

The examples below all work in one throwaway repository: a folder with a single file, committed once. The two export lines stand in for the name and email you set once with git config --global, and fix the date so the commit hashes are the same on every run.

ExampleOne file, one commit: where every example below starts
export GIT_AUTHOR_NAME=Reader GIT_AUTHOR_EMAIL=reader@example.com GIT_AUTHOR_DATE=2026-10-12T10:00:00
export GIT_COMMITTER_NAME=Reader GIT_COMMITTER_EMAIL=reader@example.com GIT_COMMITTER_DATE=2026-10-12T10:00:00
mkdir ask-docs && cd ask-docs && git init -q -b main
printf 'chunk_size = 400\n' > settings.py
git add . && git commit -q -m "Add the settings"
git log --oneline

Throwing away an edit you have not staged

git restore <file> copies the staged version of the file over the one on disk. Nothing was staged here, so the staged version is the one in the last commit, and the edit is gone.

ExampleAn edit nobody wants, and what git says about it
printf 'chunk_size = 999999\n' > settings.py
git status --short

git status --short uses two columns. The left one is the staging area and the right one is the file on disk. M with a leading space means the file on disk differs and nothing is staged.

ExampleThe same edit, taken back
git restore settings.py
cat settings.py
git status --short
echo "(git status printed nothing, which is what a clean working tree looks like)"
  • The file came back exactly. cat shows the committed line again.
  • The last status printed nothing. An empty answer from git status --short means there is nothing to report.

Unstaging without losing the edit

After git add, the change exists in two places: on disk and in the staging area. --staged takes it out of the staging area and leaves the file alone, which is what you want when you staged the right change at the wrong moment.

ExampleStaged, so the mark is in the left column
printf 'chunk_size = 200\n' > settings.py
git add settings.py
git status --short

The left column is the staging area, and it holds the change. The right column is blank because the file on disk and the staging area agree.

ExampleOut of the staging area, still on disk
git restore --staged settings.py
git status --short
cat settings.py
  • M became M. The mark moved from the left column to the right one: staged, then not staged.
  • The file still says 200. --staged never touches the working tree, which is exactly the difference between the two commands.
  • Nothing was lost. You can stage it again with git add whenever you are ready.

Undoing both at once

Passing both --staged and --worktree puts the file back to the last commit in one command. --source=HEAD says which version to take: HEAD is the commit you are on.

ExampleOne change staged and a different one on disk
printf 'chunk_size = 200\n' > settings.py && git add settings.py
printf 'chunk_size = 50\n' > settings.py          # staged 200, on disk 50
git status --short

MM is a mark in both columns: the staging area holds 200 and the file on disk holds 50.

ExampleBoth columns cleared in one command
git restore --source=HEAD --staged --worktree settings.py
git status --short
echo "(git status printed nothing)"
cat settings.py

There is no status line left and the file holds the committed value. Both edits are gone, and neither was ever in a commit, so there is nothing anywhere to get them back from.

Removing a file git has never seen

A file you have created but never added is untracked. Git is not keeping any version of it, so git restore has nothing to restore from and says so.

ExampleTwo paths git is not tracking
printf 'OPENAI_API_KEY=your-key-here\n' > .env       # created, never added
mkdir -p data && printf 'id,text\n1,hello\n' > data/notes.csv
git status --short

?? in the left of the status marks an untracked path, and data/ is listed as a folder because everything in it is untracked.

Examplegit restore only knows files git is tracking
git restore .env

The error is the useful part: pathspec '.env' did not match any file(s) known to git. Git is not refusing; it has never heard of the file.

git clean is the command for these. Run it with -n first, which lists what would go without deleting anything. -f is required to make it act, and -d includes folders.

ExampleWhat would go, with nothing gone yet
git clean -n -d

Both paths are named and both are still on disk. -n reports and stops.

ExampleThe same two paths, removed
git clean -f -d
ls -A
git status --short
echo "(clean)"

-f is what makes it act and -d is what lets it take the folder. What is left is the repository and the one tracked file.

Watch out. git restore and git clean delete work that was never committed, and git keeps no copy of it. There is no reflog, no stash and no undo for either. When you are not sure, commit the mess on a branch first, or stash it, and throw it away later when you are.

The older spellings you will meet

git restore arrived in Git 2.23. Before it, both jobs were done by commands that also did other things, and most tutorials written before 2020 use those. They still work and do the same thing here.

ExampleThe old spelling of git restore, and the file it brought back
printf 'chunk_size = 200\n' > settings.py
git checkout -- settings.py          # the old way to write: git restore settings.py
cat settings.py

git checkout with -- before the file name means the file, not a branch of that name, which is the sort of thing git restore was invented to remove.

ExampleThe old spelling of git restore --staged
printf 'chunk_size = 200\n' > settings.py && git add settings.py
git reset HEAD settings.py           # the old way to write: git restore --staged settings.py
git status --short

The staged change became an unstaged one, which is what git restore --staged did earlier, and git prints a line about unstaged changes to say so.

What you wantCurrent commandWhat you will see in older material
Throw away an edit on diskgit restore <file>git checkout -- <file>
Unstage, keep the editgit restore --staged <file>git reset HEAD <file>
Both at oncegit restore --staged --worktree <file>git checkout HEAD -- <file>
Delete untracked filesgit clean -fdthe same

Where these come up

  • An edit made in the wrong file. You changed settings.py while meaning to change the copy under notebooks/. One restore and it never happened.
  • A stray git add .. It swept up a file you did not want in this commit. git restore --staged on that one file leaves the rest staged.
  • A folder full of generated output. Caches, checkpoints and sample files that were never tracked go in one git clean -fd, after you have read git clean -nd.
Try it yourself
  • Stage one file and edit a second one, then run git status --short and name which column each mark is in before you read the answer.
  • Run git restore . with a dot instead of a file name. It undoes every unstaged edit in the folder, which is why the dot deserves a pause.
  • Create a file, run git clean -n, then add the file and run git clean -n again. Tracked files are never listed.
PreviousForks

This is what real progress feels like.