Reputation: 856
I am on a myBranch
and doing some changes that include renaming files.
I want to experiment, and then possibly wipe all my changes (restore to latest commit/HEAD) if something goes wrong.
Normally I would do:
git checkout -- .
and that'd be it.
However it has problems with removing renamed staged files for commit (also removing them from my working directory).
So the scenario I am dealing with when I want to reset, is:
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
renamed: old_file.txt -> new_file.txt
...
< files >
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)
...
< files >
There are potentially a lot of different files.
How do I restore to head discarding all commited and uncommitted changes that include renaming files?
The cleanest solution I came up with was:
git reset
git clean -f
git checkout -- .
Upvotes: 2
Views: 435
Reputation: 487755
Just remove any extra unwanted copies, after which your git checkout -- .
trick will work fine.
If you are very sure you won't lose any work you want saved, you can do git reset --hard
to do everything at once (but note that this reaches out across the entire working tree, not just whatever sub-directory you might be in).
In Git, file renaming operations aren't recorded anywhere. They are detected based on content. Step back from your immediate problem of "restore working tree contents" for a moment and consider what happens when you have two existing Git commits, which hold two different snapshots-in-time of all your files.
In the old commit, you had files A, B, and C. We put this commit on the left side. In the new commit, you have files B, C, and N. We put this commit on the right side. Here they are:
old new
--- ---
A
B B
C C
N
When Git compares the left side to the right side, it sees the two B files and the two C files and says to itself: Aha, those must be the same files each time, perhaps with different contents. I'll compare old-B and new-B; then I'll compare old-C and new-C. If the paired-up files are the same, I will say nothing. If the paired-up files differ, I will tell you what is different.
That leaves old-A and new-N. Before Git just says delete file A, and create N with all-new contents you can—and Git automatically does, since Git 2.12 or so—have Git check: is what's in N a lot like what was in A?* If so, Git will say: rename file A to N and then, if needed, also show what else to change to make file A become file N.
I'm going to go very quickly over something important here: Git actually has three copies of each file, not just two. There's one frozen-for-all-time copy of each file in the current commit, then one copy of each file ready-to-go into the next commit you could make right now stored in Git's index, and last, there's one copy of each file as an ordinary file, so that you can see it and work with it.
The frozen copy is in a special, read-only, Git-only format, automatically de-duplicated against all other commits. The index "copy"—which isn't really a copy as it uses the frozen format with de-duplication—is not directly visible.1 Only your working tree copy is visible: that copy is in the format that all the ordinary, non-Git programs on your computer can use, so when you look at your directory, that's what you see.
Git doesn't use this working tree copy most of the time: it just copies it out from a commit (by first copying the commit to Git's index, then un-freezing the index copy into the working tree) when you check out that commit. The git add
command, however, copies the working tree copy back into Git's index, replacing the old index copy. That's why you have to git add
a file even if Git already knows about it: Git uses the index copy, not the working tree copy, when making new commits.
To rename a file, you normally rename the index copy and the working-tree copy at the same time. (You cannot change any part of any existing commit, so that question never even comes up.) Normally, you might use git mv
to do this. However, all that really matters is that the index copy of a file named path/to/file
is now gone, and instead, the index now contains a file named new/name/of/somefile
. Likewise, the working tree file path/to/file
is now gone and instead, the working tree holds a file named new/name/of/somefile
.
1Actually, you can see the index contents directly, using git ls-files --stage
. It's just that this isn't something normal users really ever want: the ls-files
command is meant for writing Git commands, not for everyday use. For everyday work, it's much more useful to see the kind of stuff that git status
prints.
git status
When you run git status
, Git runs two internal git diff
operations, with rename detection always enabled:
First, Git compares the HEAD
commit's files (which are all frozen forever in time in the commit that HEAD
names) to all of the files in the index. If some file is gone on the left, but some name appears on the right that isn't on the left, Git checks: is that new right-side file really just a renaming of the left-side file? If so, git status
will tell you that in the changes staged for commit, some file is being renamed.
For each file that's the same in HEAD
and the index, git status
says nothing at all. For each file that doesn't match in some way, git status
tells you about the way it doesn't match. These are the changes staged for commit. They are not actually committed!
Then, Git compares all the files in the index to the files in your working tree. If some file is gone from the index, but some new name(s) show up in the working tree, Git will check to see if that could be from renaming a file.
As before, when the index and working tree copies don't match, git status
says something about this. These are the changes not staged for commit. Committing actually uses the copies of files that are in Git's index, rather than the working tree copies, so that's why these are not staged.
So, just as with any two commits, Git doesn't actually store a rename operation. It just says: Huh, left side had file A, right side has no A, but right side has new file N. Let's see if we can build file N by renaming A. If so, Git calls for renaming A to N.
Hence, if you have in your working tree some extra file F that shouldn't be there, and you remove F, well, now it's gone. If you have a missing file G, you can restore it from the commit, and now it's there. Now there's no rename. If you want the rename, you just need to remove G and create F (with the right contents) and now Git will compare the left (has F, lacks G) to the right (lacks F, has G) and oh hey, look, a rename!
This means that all (?) you need to do is manipulate the index and working tree contents. To add a new file to the index, create the file in your working tree and run git add file
. To remove a file from the index, use:
git rm file
: removes the file from the index and from your working tree, orgit rm --cached file
: removes the file from Git's index but leaves it alone in your working tree.The git restore
command (mentioned in git status
output) works by copying files from:
and placing the copy in:
Your task is to create the right files under the desired names, and remove files stored using any undesired names.
git reset --hard
Using git reset
can make this painless, but be aware that git reset
can be quite destructive. We mentioned earlier the whole thing about Git's index holding the copies of files that will go into the next commit. The reset
command does one, two, or all three of the following things:
First, git reset
affects the current branch name.
You must tell Git how to affect the name. If you don't say anything, Git assumes the effect is to "reset" it to HEAD
, which is where it already is. So the default reset is a do-nothing reset. This isn't useful by itself, but if you tell git reset
to go on to step 2, or steps 2 and 3 both, it becomes useful.
If you use --soft
, Git stops here, after doing step 1. Otherwise it goes on to step 2.
Then, git reset
resets Git's index.
Remember, the index already contains every file that should go into the next commit. If you haven't done anything to the index, the index's files are those that were copied out of the commit you last checked out. If you have done things to the index since then, the index has new versions of those files, and/or has some file names changed in it, and/or has new files and/or is missing old files (and a rename is the same as removing the old name and creating a new name with the same contents).
The reset command will now make the index contents match the commit contents. If you chose some other commit in step 1, this is this other commit's contents. If not, this is the same commit's contents—so this step is really only useful if you've updated the index's files in some way. However, if you want Git to go on to step 3, you have to let it do this step 2.
If you use --mixed
, or the default, Git stops here.
Last, git reset
resets your work-tree. This actually happens in combination with step 2: whenever Git has to remove an index copy of a file in step 2, it will also remove the work-tree copy of the file. If Git has to add an index copy of a file in step 2, it will wipe out the contents of the copy in your work-tree and replace it with the (defrosted) copy from the updated index. If the file doesn't exist in the index, though, it's untracked and Git won't touch it here in step 3.
You only get step three if you ask for it with --hard
. If you have fiddled with the index contents, or use two separate git reset
s, one with --mixed
and then a second with --hard
, this can leave some files behind in your working tree, because step 2 might have made them untracked. If they're in the way at this point, though, you just need to remove them with the ordinary everyday file remover on your system.
The main thing to remember about git reset
is that it will, optionally, reset both Git's index (which usually contains files you can recover from somewhere, since mostly they came out of some earlier commit) and your working tree files. Those working tree files are not in Git and if they're not from some commit, you cannot get them back from Git. So before using git reset --hard
, be very sure you won't lose a working tree file that you want to keep.
Upvotes: 3