0
India@Abhishek MINGW64 ~/Desktop/Vap class (webcss)
$ mkdir styles
mkdir: cannot create directory ‘styles’: File exists

and yet:

India@Abhishek MINGW64 ~/Desktop/Vap class (webcss)
$ git rm -r --cached  styles/
fatal: pathspec 'styles/' did not match any files
torek
  • 448,244
  • 59
  • 642
  • 775

1 Answers1

0

The mkdir output shows that the name styles exists in your working tree.

The git rm output shows that there are no files in Git's index whose pathname begins with styles/.

These are not inconsistent! mkdir cares only about whether the directory (or folder, if you prefer that term) styles/ can be created in the working tree. Either that directory (or folder) does in fact already exist, or a file with that name already exists.

The git rm command, however, was told to remove files appearing in Git's index (--cached) whose name starts with styles/. So:

  • Perhaps styles is an existing file in your working tree, which may or may not exist in Git's index. The mkdir command cannot create a new directory (folder) with that name because the file is in the way. Regardless of whether that file is also in Git's index, there are no files whose name starts with styles/: there is at most one file whose name is just styles, not styles/.

  • Or, perhaps styles is an existing directory (folder) in your working tree. Nonetheless, Git's index—which I'll talk more about in a moment—does not contain any files whose name starts with styles/. You would get this in several cases.

So, the first thing to do here is to check whether you have a file named styles. If so, that's blocking the existence of a directory named styles and you will have to do something about the file.

We might assume that this is not the problem. If it were, you probably would not have posted this question. But maybe it is the problem, since you did not mention checking for this first, nor show anything to demonstrate this. This sort of thing is why writing a good question is a good idea. See How do I ask a good question? If you write a good question, you can often get a short answer. (cough )

Git's index

Git's index is an important and central construct; understanding what it is, and does for you and Git, is almost as necessary as understanding what a Git commit is and does for you and Git. The index is so important—and/or so poorly named—that it has three names in Git:

  • index, which we have already seen a few times, and which has no apparent meaning;
  • staging area, which is mainly how you use the index: to "stage" files for commit; and
  • cache.

The last term is somewhat obsolete, but still pops up in places like the --cached flag to git rm. Many Git commands accept --staged as a synonym for --cached, but for some reason, git rm still doesn't.

The short and easy to remember description of the index—which is slightly incomplete, as it misses out on the index's role in merge conflicts—is that it holds your proposed next commit.

That is, when working with Git, we start by picking some particular commit that Git should extract:

git switch master

or:

git checkout develop

or:

git switch --detach a123456    # raw hash ID

When we do this—that is, when we check out any given, existing commit—Git extracts, from that commit, the snapshot-of-all-files that are stored in that commit. These stored files are read-only: every Git commit acts like a tar or zip archive of every file, and once made, no part of any commit can ever be changed (not even by Git itself). So the files inside them—which are also compressed and de-duplicated to save lots of space—have to be extracted into a working area, so that they can now be normal everyday files that you can work on / with.

The working copies of each file are stored in what Git calls your working tree. This part of Git is actually really simple and straightforward. If you download a tarball or zip archive, you have to un-archive it to get the files out. The same goes for commits: you have to have Git un-archive them, to get the files out. The extracted files go somewhere, and in Git, that's your working tree. So that explains why there are two copies of every file:

  • there's the compressed, de-duplicated copy in the commit; and
  • there's a version you can actually use in your working tree.

But here's where Git is weird. For no particularly obvious reason, Git keeps a third copy of each file.1 The third copy that Git keeps is in the compressed-and-de-duplicated format, so when it's files that just came out of some commit, they don't take any space,2 and the obvious objection to keeping a third copy kind of vanishes.

This extra index, or staged, copy is what Git will put into the next commit you make. So now you work merrily3 away in your working tree, fixing bugs, adding features, doing whatever it is you may be doing. Once you're ready, you must run git add. Why do you have to run git add so much? Because of Git's index, aka staging area. The git add step tells Git: Read the working tree copy. Compress it. De-duplicate it. Stick that compressed, de-duplicated file into your index/staging-area, ready to go into the next commit. And so, Git does that, and now the index / staging-area has the updated file, which—except for being in Git's format—matches the working tree copy now, instead of matching the current commit's copy.

This, too, is the big secret of git status. When you run git status, Git runs two fast git diff operations:4

  • The first one compares the files in the current (or HEAD) commit to the files in Git's index / staging-area. For whatever is the same here, Git says nothing about these files. For whatever is different here, Git says that this file is staged for commit.

  • The second git diff compares the files in Git's index to the files in your working tree. For whatever is the same here, Git says nothing. For whatever is different here, Git says not staged for commit.

Since you can, in fact, copy a file into Git's index, and then change the file again, you can get one file that is both "staged for commit" and "not staged for commit". That just means that the three copies are different.5

So this all shows us how Git's index works: The index holds the proposed next commit. By running git add or git rm, you update the proposed next commit. You can't change the contents of the current commit: those are set in stone. You can, however, change the contents of your working tree—those are just ordinary computer files after all—and you can change the contents of Git's index too. You use your working tree files to update your index copies. Commands like git add -p use your working tree copy to partially update the index copy, getting you into that state where HEAD, index, and working-tree copies are all different.


1Other version control systems (VCSes) often do have something vaguely similar to Git's index, but they hide it. You don't have to know about it. You can't use it directly, like you can use Git's index. They tend not to have it as a "third copy" as they don't use Git's internal storage format.

2Technically, they take a little space for cache and other data, but it's small enough to mostly ignore. And, as in footnote 1, other VCSes do the same thing here: it's just hidden. Those other VCSes tend not to have the next-commit-able copy pre-built the way Git does, though.

3Or other adjective of your choice.

4They are fast because we have Git run them with --name-only or --name-status internally: they just check to see is this file different, without trying to figure out exactly what might be different.

5Technically, it can mean that the working tree copy matches the HEAD-commit copy, and just the index copy is different. Try adding one line to a file, running git add, and then removing the line. Run git status when in this state—with the "put the file back" change not yet add-ed—and observe the status. Then run git add again, and observe that both lines talking about this one file vanish. That's because changing the index copy back made all three copies match again, because the working tree copy matched the HEAD copy.


Now that you know about the index, we can see some possible problems

Suppose that styles does exist as a directory in your working tree.

Perhaps you just made it just now. If so, there are no files in the directory: it is empty. (If you're on a Mac and using the Finder, the Finder might create styles/.DS_Store at any point, so watch out on macOS. Other past or future systems might do something similar too. But this is all beside the main point.) Even if you've just created a few files in that directory, until you run git add on them, they won't be in Git's index.

There's another, smaller point to note here: Git's index cannot hold directory names.6 Instead, Git treats a file name like styles/a.css as ... a file named styles/a.css, complete with forward slash. This is true even on Windows, where the file's official name is a.css in a folder named styles, referred to as styles\a.css: Git's index still holds styles/a.css as the file's name. Git knows how to split up its own long, slash-in-the-middle names into folder and file parts when working with your OS, but the names in the index are just big long text strings. The index's names are always case-sensitive, too: even if your OS says that files readme.txt and README.TXT and ReadMe.txT are "the same file", Git's index insists that these are three different names.7

When you run git rm --cached, you are telling git rm that it should work only on files in Git's index. Any files in your working tree should be left alone. This allows you to remove a file from the index entirely, while leaving it intact in your working tree. That in turn allows you to make sure that the file won't be in the next commit, since the index stores your proposed next commit. Just make sure that the file stays out of Git's index, and then make the commit, and now the file isn't in the new commit and you're all set.


6There's no good technical reason for this. It just doesn't. This might get fixed in some future Git version, maybe.

7This causes endless headaches on typical macOS and Windows systems. It's still the Right Thing to Do, because those differently-named files can and do get created on Linux systems, and then stored into Git repositories, after which there's a problem. The index should do what it does do now; it's the tooling around the index that needs improvement.


Submodules

The last special wrinkle here occurs with submodules.

A Git submodule is Git's way of storing a reference to some other Git repository. Git will utterly refuse to store an actual repository inside another repository (for good and valid reasons). So if:

path/to

is a directory (or folder) path containing a directory (or folder) to in your working tree, and path/to/sub is also a directory/folder and path/to/sub/.git exists and refers to a Git repository, then path/to/sub/ is the working tree for that other Git repository.

If, at the same time, .git here, where path/to/ exists, exists and refers to a Git repository, then this is the working tree of this repository. So we have "nested repositories": this repository, in path/to/, seems to contain another repository, path/to/sub.

But Git forbids this. A repository literally cannot contain another repository. Git resolves the tension here by decreeing that path/to/sub is, in this repository, to be treated as what Git calls a gitlink.

A gitlink is simply a special "file" of type "gitlink".8 Git combines this with an entry in a file named .gitmodules to form a submodule. The submodule just tells the "outer" Git: Make an empty directory here, and later, do a git clone and git checkout inside the directory.

If you have a styles gitlink, then, Git will have created an empty directory named styles. The directory itself will not be filled in unless and until some other Git repository is created—by you, or by Git; this part does not matter—in that directory. Once you or Git do create such a Git directory, the outer Git will refuse to put anything other than the special gitlink styles entry into its index. There will therefore never be any styles/ files to remove. Commits you make in the outer Git repository, while styles in the outer Git's index is this gitlink, will just hold the gitlink.9

Hence, any attempt to remove any files named styles/whatever are going to fail: the existence of the styles gitlink in this Git's index blocks the existence of any styles/whatever files in this Git's index.

If you don't want the submodule-ness, you must remove the gitlink (and typically you want to remove the .gitmodules entry as well at this time) and make a new commit. You can, before you make the new commit, also then add the files from the submodule's working tree—after moving or removing the .git that makes the outer Git treat the inner one as a submodule, though for sanity's sake I'd recommend de-submodule-ing as one commit, then adding stuff in as a second commit. But you only want any of this if you really do want to eliminate the submodule.

(Do you want that? I don't know! "But it is also said: Do not meddle in the affairs of wizards, for they are subtle and quick to anger. The choice is yours...")


8A gitlink is an index or tree entry with mode 160000. This is essentially mode 120000|040000. Regular files are mode 100755 or 100644. 120000is a symbolic link, and040000is reserved fortree. Tree IDs are not allowed to appear in the index, at least not at this particular level. That's why footnote 6 is somewhat puzzling. If you manage to shove a mode 040000entry *into* the index, though, some part of Git's index-handling code winds up converting it to amode 160000` entry, and everything goes rather haywire.

9The "value" associated with that gitlink will be a commit hash ID. This represents the hash ID that the outer Git should use when it runs:

(cd styles && git checkout $hash)

You update that outer Git index's gitlink hash ID by:

  • entering the inner, or submodule, repository (cd styles);
  • using Git commands to select, or even create, a commit, such that the desired commit is the HEAD commit in the submodule;
  • leaving the submodule to return back to the so-called superproject, where the gitlink exists: this is the outer Git; and
  • running git add to make the superproject read the hash ID from the submodule and update it in the superproject's index.
torek
  • 448,244
  • 59
  • 642
  • 775