6

After staging a file(tracked file) I realized it needed some changes, so to unstage it I used git rm --cached <filename>. Consequently, I lost all the changes. Is there a way to undo it?

nova
  • 63
  • 1
  • 4
  • 1
    `gir rm --cached` does _not_ unstage a file in Git, it unstages the addition of that file to the repository. What changes did you lose, and are you certain that you really lost anything? – Tim Biegeleisen Oct 24 '19 at 11:10

4 Answers4

10

git rm --cached only removes the new file from the index and not from the working tree. Be careful doing it that way. If this was an existing file you just staged the deletion of the file.

You can add it again with git add <filename>.

A more common way of unstaging changes is to use git reset HEAD <filename>.

But... it's not necessary to unstage a file to add further changes. You can just make further changes and stage those again using git add <filename>.

Matthijs P
  • 1,144
  • 1
  • 9
  • 17
2

Git's use of the phrase staged changes or to stage some changes can get very confusing.

To avoid getting confused, here is what you need to know:

  • Git has a thing that Git calls, variously, the index, the staging area, or sometimes—rarely now—the cache. These three names all refer to one single thing. Here, I'll just call it the index.

  • The index initially contains a copy1 of each file that is in the commit that you checked out. (This current commit is also called HEAD.) These files are in a Git-only format. You can't see these files. (This is a slight overstatement—there are some special Git tricks to see them—but normally they're invisible.) The files that you can see are in your work-tree, which holds ordinary everyday files.

  • When you make your next commit, Git will use the files that are in the index at that time. That is, when you run git commit -m "some message" (or after git commit without -m collects a message from you), Git will then, at that time, package up all the files that are in the index at that time, and use those to make the new commit.

  • Hence, if you've changed or replaced a file in your work-tree, you must first copy it back into the index. Otherwise Git will just re-use the original file.

When you run git add file, Git takes the work-tree copy, compresses it into the special Git-only format, and stuffs the compressed copy into the index. Now the index copy no longer matches the HEAD copy, but it does match the work-tree copy.

If you change the work-tree copy again, now all three copies are different! At this point git status will tell you that you have "changes staged for commit" and also "changes not staged for commit".

You can change the work-tree copy to anything at any time. This has no effect on the index copy.

You can remove the work-tree copy without removing the index copy, using any tool on your computer that removes a file. You can also remove the index copy without removing the work-tree copy, using git rm --cached. And, you can remove both copies using git rm. None of this affects any existing commit—no existing commit can ever be changed! But removing the index copy does affect the next commit, because when you run git commit, Git will package up all the files that are in the index at that time, so if the file is gone from the index, it won't be in the next commit.

Since what's in the index is what will be in the next commit, that's a pretty good description of the index: The index is, mostly anyway, what will go in your next commit. The index does have some additional roles to play, especially during merges, but we won't cover those here at all.


1Technically, the index actually holds references to the copies. But the effect is pretty much the same as if the index held actual copies, at least until you start diving into how to manipulate the index directly using git update-index.


How to "unstage changes"

To get the file from the current (HEAD) commit back into the index in the form it has in the current commit, you can use git reset -- file. The -- here is only needed if the file name resembles a git reset option or a branch name. For instance, if you have a file named master and a branch name master, git reset master looks like you want to use the branch name, so you must write git reset -- master to tell git that you mean the file master.

So: git reset -- file copies the file from the HEAD commit into the index. This is true even if you removed the index copy: reset will just put it back. It's also true if the file isn't in the HEAD commit. If you've added file F to the index, and then decide it should not be in the index after all, you can git reset -- F and that removes it from the index.

(You can also git rm --cached F if you prefer: that has the same effect at this point. But if you just want to make the index copy match the HEAD copy, use git reset because that works for both cases: there is a HEAD copy, or there isn't.)

How do you know what is staged?

Again, you can't actually see the copy of the file that is in your index. So how do you know whether it matches some other copy? The answer is: git status tells you, in a short and useful way.

Let's say you have three files in your work-tree called README.md, main.py, and util.py. The first two of these files came out of the HEAD commit, and you've just created util.py yourself now, so it's not in the HEAD commit and not in the index.

The git status command runs two comparisons:

  • First, it compares every file in HEAD to every file in the index. So this compares HEAD:README.md (the HEAD copy) to :README.md (the index copy).2 Then it compares HEAD:main.py to :main.py.

    For each file that is the same, git status says nothing.

    For each file that is different, git status says that there are changes staged for commit. If the file is completely gone from the index, you've staged a delete. If the file in the index is totally new, you've staged a newly-added file.

    So, if you know what was in the HEAD commit, you now know which files are the same in the index: anything git status did not mention.

  • Next, having told you what's different or not in HEAD vs index, git status goes on to compare each file in the index to each file in the work-tree. So here, it will compare :README.md to README.md, :main.py to main.py, and discover that there's a util.py that's not in the index.

    For each file that is the same, git status says nothing.

    For each file that is different, git status reports changes not staged for commit. That's right because git commit isn't going to use the work-tree, it's just going to use the index.

    For a file like util.py, that's in the work-tree but not in the index, git status reports it as an untracked file. That's what an untracked file is: a file that is in the work-tree, but not in the index.

Note that if you remove a file from the index, it is instantly untracked! If you git add the file so that it's in the index, well, now it's tracked.


2This funny HEAD:file and :file syntax is specific to Git itself, and only works with some Git commands. One of those is git show: you actually can see the index copy of README.md using git show :README.md, for instance. Since git show is a user-oriented command, you can do this with pretty good safety: git show HEAD:file, git show :file, and git show <commit:>file are all pretty good ways to see a specific copy of a specific file that's been turned into its internal Git form, either saved in a commit, or ready to go into a commit.

You can also see the names of all the index copies of files using git ls-files. In a big repository, this prints a lot of names! It is not meant for normal everyday use and is not a very user-friendly command, though.


Commits read the index; git checkout writes to it

To make a new commit, you simply copy the right files into the index and run git commit. The index already has all the files from the current commit, so you only need to update any files that you want to be different, or add any new files you want, or remove any files you want to delete. Then you git commit and make a new commit. This new commit is now the HEAD commit, and now the HEAD commit and the index match.

If the HEAD commit and the index match, and those two match the work-tree, then everything is "clean" and you can easily switch to another commit, using, e.g.:

git checkout otherbranch

(or the new Git 2.23 git switch command, which is like a friendly variant of git checkout branch that doesn't have all the other Git tools shoved into one command—git checkout can do somewhere between three and seven different jobs, depending on how you count).

This checkout, assuming it succeeds,3 has to:

  1. fill your index with all the files from the tip of otherbranch (and remove any irrelevant ones);
  2. fill your work-tree with all the files from the tip of otherbranch (and remove any irrelevant ones); and
  3. make the name HEAD refer to the tip commit of otherbranch.

Once that's all done, your HEAD commit, index, and work-tree again all match.


3The git checkout command can sometimes succeed *even if the index and work-tree are not "clean". Clean is a poorly defined term here, but to see more about when git checkout will let you switch branches while carrying modifications, see Checkout another branch when there are uncommitted changes on the current branch.


Conclusion

You can't see the index copies of files! What you can and should do is use git status, which compares the HEAD files to the index copies. In a big project, with hundreds, thousands, or even millions of files, Git will say nothing about almost all of them. It will only mention the ones that are different. That's much more useful.

Whenever your HEAD, index, and work-tree all match, git status says nothing. When git status sees some things that don't match, it prints file names under changes staged for commit and/or under changes not staged for commit depending on whether it's the HEAD and index copies that disagree, or the index and work-tree copies that disagree, or both.

git add copies files into the index. If they were already there before, well, now they're overwritten with the copy from the work-tree. If they weren't there before, well, now they are.

git rm removes files from the index. Without --cached, it also removes the same files from the work-tree. With --cached, it leaves the work-tree copies alone—which is fine for now, but a later git checkout might need to remove the work-tree copies!

Git will, in general, try fairly hard not to destroy any work-tree files with precious data. What's "precious", though? Well, if work-tree file util.py has been added and committed and matches HEAD:util.py and :util.py, at this point, util.py is not precious any more, because if you want it, it's there in the commit. So you can git checkout an old commit that doesn't have util.py and Git will feel totally safe in removing util.py from your work-tree. Just git checkout the latest command it's back.

Some Git commands, including git reset --hard, will destroy work-tree files. The theory behind git reset --hard is that you want to reset both the index and your work-tree. The default mode, which is git reset --mixed, resets only the index copies.4 That's why git reset -- file "un-stages" the file—copies HEAD:file into :file–but doesn't touch the work-tree copy of file.


4Peculiarly, git reset --soft, git reset --mixed, and git reset --hard only allow you do do every file. When you want to do just one file, using git reset -- file, it always uses --mixed. For other cases, you have to use one of the many alternate modes of git checkout. Git 2.23 introduces a new command, git restore, that is intended to make this less confusing. Time will tell whether it does: git restore is technically still experimental.

torek
  • 448,244
  • 59
  • 642
  • 775
1

You need to add again. Here is a sample set of commands.

echo testmodify>> FOO //Appends "testmodify" to FOO
git add FOO // Stage FOO to commit
git rm --cached FOO // Unstage FOO
git add FOO // Stage FOO

FOO still contains "testmodify"

EncryptedWatermelon
  • 4,788
  • 1
  • 12
  • 28
0

As per my experience. I ran this command "git rm --cached ." and my whole project files got removed so before pushing any commit, I stash the code. Simple and easy way to get rid of this issue.

git stash