git restore
git restore is the modern command for undoing changes to files in your working directory and staging area, without touching your commit history. It was added in Git 2.23 to take over one half of the job that git checkout used to do — restoring file contents — so that branch switching and file restoration are no longer tangled up in a single overloaded command. If you’ve ever accidentally edited the wrong file, staged something you didn’t mean to commit, or wanted to bring back an old version of a file, git restore is the tool built exactly for that.
Overview / How it works
To understand git restore, you need to picture the three places a version of a file can live in Git: the working tree (the actual files on disk you edit), the index (also called the staging area — a snapshot of what will go into the next commit), and a commit (a permanent snapshot already saved in history, starting from HEAD, the commit your current branch points to). Every commit object stores a pointer to a tree object, which represents a snapshot of your project’s directory structure; a tree in turn points to blob objects (raw file contents, content-addressed by a SHA-1 hash) and other trees (for subdirectories). None of this — blobs, trees, or commits — is ever modified once written; Git only ever adds new objects.
A branch, like main, is nothing more than a small file containing a commit hash — a lightweight, movable pointer. HEAD normally points at a branch (not directly at a commit), and the branch points at the current commit. When you run git restore, none of that pointer chain moves. HEAD stays put, the branch stays put, and no new commit is created. All git restore does is copy file content from a source (by default the commit at HEAD) into the working tree, the index, or both — overwriting whatever was there before.
This is the key mental model: git restore has two independent targets, controlled by two flags. --worktree (the default when you give no flag) rewrites files on disk, discarding any uncommitted edits in those files. --staged rewrites the index, which is how you “unstage” a file you added with git add without touching what’s on disk. You can combine both flags to reset a file completely back to how it looked in the source commit, discarding both staged and unstaged changes at once.
Syntax
git restore [--source=<tree>] [--staged] [--worktree] [--patch] [--] <pathspec>...
| Flag | Meaning |
|---|---|
--worktree / -W |
Restore files in the working directory. This is the default target if neither --staged nor --worktree is given. |
--staged / -S |
Restore the index — this is how you unstage a file. Leaves the working directory untouched. |
--staged --worktree |
Restore both the index and the working tree from the source, fully discarding local changes to the file. |
--source=<tree> |
Choose where to restore from — a commit hash, branch name, or something like HEAD~2. Defaults to HEAD for --worktree, and to the index’s current state has no meaning here since --staged also defaults its source to HEAD. |
--patch / -p |
Interactively choose which hunks (chunks of a diff) to restore, instead of the whole file. |
-- |
Separates flags from pathspecs, useful when a filename could be confused with a branch name. |
<pathspec> |
One or more file paths, or . for everything in and below the current directory. git restore always needs at least one pathspec — it never switches branches. |
Examples
Example 1: Discard an unstaged edit
git status
On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: src/login.js
git restore src/login.js
The first command shows Git itself suggesting git restore as the fix. Running it with no flags targets the working tree by default: Git reads the blob for src/login.js out of the HEAD commit’s tree and overwrites the file on disk with that content. The command prints nothing on success — a quiet git status afterward confirms the file is back to “clean.” Your edit is gone permanently; there is no undo for this.
Example 2: Unstage a file you added by mistake
git add .
git status
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: .env
modified: src/login.js
git restore --staged .env
Here git add . swept up a local .env file that should never be committed. git restore --staged .env restores the index entry for that path from HEAD — since .env was never committed before, restoring “from HEAD” for a new file effectively removes it from the index, returning it to untracked. Crucially, the file itself is untouched on disk because --worktree was not used; only the staging area changed.
Example 3: Recover a file from an older commit
git log --oneline -- config/settings.yml
a1b2c3d fix: correct database pool size
9f8e7d6 feat: add settings.yml with defaults
git restore --source=9f8e7d6 -- config/settings.yml
Suppose a later commit broke config/settings.yml and you want the version from when it was first added. --source=9f8e7d6 tells Git to pull the blob for that path out of the tree belonging to commit 9f8e7d6 instead of the default HEAD, and write it into the working tree. The file is now modified relative to HEAD and shows up under “Changes not staged for commit” — you still need git add and git commit to make the recovery permanent.
How it works step by step
For a plain git restore src/login.js (implicit --worktree, implicit --source=HEAD): Git resolves HEAD to a commit object, reads that commit’s tree object, walks the tree to find the entry for src/login.js, and reads the corresponding blob’s content. That content is written directly onto disk, replacing whatever bytes were there. The index entry for that path is left exactly as it was.
For git restore --staged src/login.js: the same lookup happens — resolve source (default HEAD) to a commit, tree, then blob — but instead of writing to disk, Git updates the index entry for that path to point at that blob and mode. The working tree file is not touched, so git diff (working tree vs index) will now show a difference where git diff --staged (index vs HEAD) shows none.
In both cases, no commit is created, no ref is moved, and no reflog entry is written for branches — the operation is purely a copy from a read-only source (a tree already in the object database) onto a mutable target (the index and/or the working tree).
Common Mistakes
Mistake: running git restore expecting a safety net.
git restore report.md
If report.md had unsaved, uncommitted edits, they are gone the instant this runs — there’s no trash can, no confirmation prompt. Fix: before restoring, run git diff report.md to see exactly what you’re about to lose, or run git stash first if there’s any chance you’ll want the edit back later.
Mistake: assuming --staged also reverts the working tree.
git restore --staged src/api.js
# expecting src/api.js on disk to also revert — it won't
After this, git diff will still show your uncommitted edits against the now-unstaged version. People are often confused when their editor still shows the “wrong” content. Fix: if you want both the index and the working tree reset, explicitly pass both flags: git restore --staged --worktree src/api.js.
Mistake: using bare git restore . at the repository root.
git restore .
This discards every unstaged change in the entire working tree from the current directory down — including files in subdirectories you forgot you’d touched. Fix: scope the pathspec narrowly (git restore src/), or use git restore -p to review and approve each hunk before it’s thrown away.
Best Practices
- Run
git diff <file>(orgit diff --staged <file>) before restoring, so you know exactly what will be discarded. - Use
git restore -p <file>when you only want to discard part of a change and keep the rest. - Trust the hints Git prints in
git status— it already tells you the exactgit restorecommand for your situation. - If you’re not sure you’ll want the change gone forever,
git stashit instead of restoring — a stash can be recovered later, a restore cannot. - Prefer
git restoreandgit switchover the oldergit checkoutfor new scripts and habits — the split commands make your intent unambiguous to teammates reading your shell history. - Remember
git restorenever rewrites commit history; if the bad change is already committed, you needgit revert(or, with care,git reset) instead.
Practice Exercises
- Edit two files,
src/app.jsandsrc/utils.js, but only discard the change tosrc/utils.js, leavingsrc/app.js‘s edit in place. Verify withgit statusafterward. - Stage three files with
git add ., then unstage only one of them without touching its working-tree content. Confirm withgit diff --stagedthat only two files remain staged. - Find the previous version of a file that changed two commits ago (hint:
git log --oneline -- <file>to find the commit hash before the change), then usegit restore --sourceto bring that older version into your working tree without committing it yet.
Summary
git restoreundoes uncommitted changes to files — it never creates commits or moves branch pointers.- By default it targets the working tree; add
--stagedto target the index (unstage) instead, or combine both flags to reset a file completely. - The source of the restored content defaults to
HEADbut can be any commit via--source. - It works by copying a blob’s content out of a tree object in the object database into the index and/or working tree — a one-way, destructive overwrite of whatever was there.
- Always check
git diffbefore restoring, and reach forgit stashinstead if you might want the change back later.
