Undo in your files: git restore
The interactive lesson needs JavaScript. The text below is the complete lesson.
After this lesson you can
- discard an unstaged edit with
git restore, and explain why Git cannot bring back an edit it never stored apply - unstage a file with
git restore --stagedwithout losing the edit in the file apply - bring back an older version of a file with
git restore --source, without changing history apply - choose the restore variant from where the version you want lives, and recognise when the job concerns a commit and needs reset or revert analyse
Understand: Three places, three directions
Remember the three places a file can live: the working tree, the staging area and the last commit. So far you have moved versions to the right with add and commit. Today we move them back to the left.
Picture this. You added a line about Sundays and staged it. Then you typed some junk into the same file. Now there are three different versions, one in each place.
git restore README.md copies the version from the staging area into the working tree. The card flies one step to the left, and the junk is gone. Note: the junk was never stored in Git, so this action cannot be undone.
git restore --staged README.md works one step further to the right. It copies the version from the last commit into the staging area. The working file is not touched, so the Sunday line is still there, just no longer staged.
You can also bring back a file from an older commit. git restore --source=HEAD~1 README.md copies the version from the commit before the last one. Without more options it lands only in the working tree.
In all three cases main is on the same commit as before. Restore changes files and staging, never history. To undo a commit you need reset or revert.
Watch: Clean up README.md
We start with two commits. First we add a useful line about Sundays and stage it. Watch the card move into staging.
$ echo "Closed Sundays" >> README.md && git add README.md
Then an accident: some junk lands in the same file. Now the working file, staging and the last commit are three different versions.
$ echo "asdf qwerty" >> README.md
Before you discard anything, look at what you are throwing away. git diff shows only what is not staged. Here that is just the junk.
$ git diff
Now we run git restore README.md. Watch the working card: the junk disappears, and the Sunday line stays.
$ git restore README.md
You change your mind: the Sunday line should not go into the next commit yet. git restore --staged copies the last commit into staging. The staging card changes content, but the working file keeps the line.
$ git restore --staged README.md
Now the boss wants the README without opening hours until they are decided. We bring back the file as it was one commit back. Note that this also overwrites the Sunday line in the working file, because restore without --staged always overwrites the working file.
$ git restore --source=HEAD~1 README.md
Finally we check the status. No commit was made or removed, and main is where it was. But what does status say about README.md?
$ git status --short
Tasks
- See exactly which unstaged changes you are about to discard.
- Get rid of the junk, but keep the staged Sunday line.
- Unstage README.md, but keep the Sunday line in the file.
- Make README.md in the working tree match the version from one commit back.
Practise: Transfer: three files, three problems
A repository where a bit of everything has gone wrong. Use git status and git diff to see the state before you undo anything.
Tasks
- Both README.md and todo.txt are staged. Only README.md should go into the next commit. Keep the todo.txt edit in the file.
- contact.txt was deleted by accident. Bring it back exactly as it was.
- The newest commit put a bad idea into notes.txt. Without changing history, make notes.txt in the working tree match the version from before that commit.
Check yourself: Recall it
You run git restore --staged app.txt. What happens to the edit in the working file?
- It is deleted
- It stays in the file, but is no longer staged
- It is committed
You have an edit in notes.txt that was never staged or committed, and run git restore notes.txt. Can Git give you the edit back?
- Yes, with
git reflog - No, Git never stored that version
- Yes, Git keeps every version of a tracked file
git status --short shows M config.txt: a staged change, and nothing unstaged. You want config.txt back as it is in the last commit, both in staging and in the file. What do you do?
git restore config.txtgit restore --staged config.txt, thengit restore config.txtgit revert HEAD
Review from “Three areas”: git status --short shows MM README.md. What do the two Ms mean?
- README.md was changed in two commits
- The file has a staged change, and on top of that an unstaged change in the working tree
- The file has a conflict that must be resolved
Common misconceptions
- “
git restore --stageddeletes my change.” It only resets the staging area to the last commit. The change is still in the working file. - “Git can always bring back what I discard.” Git can only return versions it has stored, that is staged or committed. An edit that was never stored is gone after
git restore. - “Restore can undo a commit.” Restore never moves a branch. Commits stay exactly where they are.
Read more
What restore reads and what it writes
git restore copies a version of one or more files from a source to a target. It never moves HEAD or a branch, so history stays exactly as it was. The options only choose the source and the target:
| Command | Reads from | Writes to |
|---|---|---|
git restore app.txt |
staging area | working tree |
git restore --staged app.txt |
HEAD |
staging area |
git restore --staged --worktree app.txt |
HEAD |
staging area and working tree |
git restore --source=HEAD~2 app.txt |
the commit HEAD~2 |
working tree |
git restore --source=HEAD~2 --staged --worktree app.txt |
the commit HEAD~2 |
staging area and working tree |
Two rules explain the table. Without --staged the target is the working tree. Without --source the source is HEAD as soon as --staged is given, and otherwise the staging area, also called the index. Paths can be a single file, a folder or . for everything below the current folder, so git restore . throws away every unstaged edit at once.
Whatever restore writes to the working tree overwrites the file without asking. An edit that was never staged or committed was never stored in Git, so no Git command can bring it back. Look first with
git diff, and usegit stashif you are unsure.
Discard only some of the changes: git restore -p
-p (patch) shows each hunk and asks before discarding it. This is handy when a file has a good fix and some debugging lines:
$ git restore -p README.md
@@ -1,2 +1,3 @@
Welcome
-Open 9-15
+Open 9-16
+Closed Sundays
(1/1) Discard this hunk from worktree [y,n,q,a,d,e,p,P,?]?
Answer y to discard the hunk, n to keep it, q to stop. s splits a large hunk into smaller ones when possible. git restore -p --staged README.md does the same for staged hunks, and git add -p is the mirror image in the other direction.
Older spellings you will meet
git restore arrived in Git 2.23 (2019). Before that, the same jobs were done by git checkout and git reset, and many tutorials and colleagues still use them:
| Older command | Same as |
|---|---|
git checkout -- README.md |
git restore README.md |
git checkout HEAD~1 -- README.md |
git restore --source=HEAD~1 --staged --worktree README.md |
git reset README.md |
git restore --staged README.md |
Note the middle row: checkout with a commit writes both the staging area and the working tree, so the old version is already staged afterwards. Plain --source only touches the working tree. The -- means "everything after this is a file name", which matters when a file and a branch have the same name. git checkout also switches branches, which is why it was split into git switch and git restore.
Untracked files are not restore's business
Restore only knows files Git tracks. Asking it about a new file fails:
$ git restore scratch.txt
error: pathspec 'scratch.txt' did not match any file(s) known to git
To delete untracked files, delete them yourself or use git clean. Always run git clean -n first: it only lists what would be deleted. The real run needs -f, which is exactly the kind of flag you should type slowly.
Stop tracking a file without deleting it: git rm --cached
Sometimes the mistake is that a file is tracked at all, for example a local .env with settings. git rm --cached removes the file from the staging area, so the next commit no longer contains it, but leaves your copy on disk:
$ git rm --cached .env
rm '.env'
$ git status --short
D .env
?? .env
The file is staged as deleted and at the same time shows up as untracked. Add it to .gitignore and commit both changes. Older commits still contain the file. If it held a password or an API key, change that secret: removing a file from tracking hides nothing that was already committed or pushed.
Restore, reset or revert?
Restore fixes files and staging. Reset moves your own branch. Revert adds a new commit that undoes an old one. The table in the lesson “Revert” shows which tool fits when.
Official documentation
- git-restore
- git-rm, see
--cached - git-clean
- Pro Git: Undoing Things
Per-Arne's Git Good!