The list of untracked files reads (this is just a straight quote of your own quote):
.gitignore .vs/ 3dengine_headeronly.sln 3dengine_headeronly/ CMakeLists.txt Debug/ Tests/ cmake-build-debug/ packages/ resources/
Now, note that there are only three names here that do not end with a slash:
.gitignore
You probably should add and commit this file, so you probably do want it to be untracked at the moment, until you add and commit it.3dengine_headeronly.sln
This file is not listed as “do not auto-add nor complain about untracked-ness”. Check it out yourself: search through your.gitignore
contents for anything resmbling3dengine_headeronly.sln
. One entry that comes close is the last line in this group:# JetBrains Rider .idea/ *.sln.iml
which lists*.sln.iml
as “do not auto-add nor complain”. But3dengine_headeronly.sln
ends with.sln
, not.sln.iml
. Another is the line*.sln.docstates
, but again, that ends with.sln.docstates
, and the file’s actual name ends with just.sln
.CMakeLists.txt
As before, there is no entry that would make Git shut up about this.
That leaves unexplained the entries ending with slashes. The problem with diagnosing this is that Git has summarized the contents of those directories, rather than listing the individual files. One can only explain this if one has the actual file names, which requires running git status
with the -u
or --untracked-files=
flag, and giving this flag the word all
to tell Git to list each untracked file individually.
Without this flag … well, let’s say, for instance, that Tests
contains:
Tests/a.b Tests/c.d Tests/e.obj
One of these three files matches the *.obj
pattern, but the other two match no pattern, so Git will complain that Tests/a.b
and Tests/c.d
are untracked. But without -uall
, the way Git will complain about this is to say that Tests/
is untracked. This is true even if every file within Tests/
is marked as “don’t complain” except for one file in Tests
or some subdirectory within Tests
: Git will summarize the one file by printing Tests/
because there are no tracked files within Tests/
, but there is at least one untracked file within Tests/
that Git has not been told “shut up about this file”.
There is one that is pretty mysterious, and that one is Debug/
. You do have this line in your .gitignore
:
[Dd]ebug/
This should cause all files within Debug
and any subdirectory inside Debug
to be not-complained-about. Using -uall
will probably shed some light on what is going on, though. (See below for why this one is such a mystery.)
Long: how git status
scans the work-tree
It’s important to realize that Git never tracks a directory itself. What listing a directory in .gitignore
does is give Git permission not to examine the directory.
To understand this we need to define what it means to track a file in the first place, and how Git examines your work-tree when it wants to collect up a list of untracked files for git status
to complain about.
A tracked file, in Git, is simply any file whose name is currently listed in Git’s index. To see which files are currently in the index, you can run git ls-files --stage
(but note that in a big directory this tends to list a lot of files). These are the files that will be in the next commit you make. Initially, the index is full of all the files that were in the commit you most recently checked out. The next commit will contain all those same files. If you overwrite one of the index copies, the next commit will contain the updated index copy—and that’s what git add
does, when you git add
a file that’s already in the index.
Hence, an untracked file is simply any file that exists in the work-tree, but that is not currently in the index. Using git rm
—with or without --cached
—you can remove any file from the index, if it’s currently in the index. Once it’s gone from the index, if you’ve kept the work-tree copy, it has become untracked.
What git status
does is to run two comparisons—two git diff
s, in effect. The first one compares the current commit frozen files to the index’s unfrozen (but Git-ified) files: whatever is the same, Git says nothing about, and whatever is different, Git says: file with pathname P is staged for commit. This is way better than listing every file in the index: it tells you about file P only if it’s going to be different in the next commit.
The second comparison is the tricky one, and is where .gitignore
comes in. To run the second comparison, Git compares the Git-ified but not-yet-frozen copy of a file that’s in the index with the regular, non-Git-ified copy in the work-tree. When the two are different, Git tells you that this file is not staged for commit. You can run git add
to copy the work-tree copy over top of the index copy, so that the index and work-tree match—and now, presumably, the HEAD
commit and index copies of the file differ, so that the next git status
will say staged for commit.
That works great for files that are in both the index and the work-tree, but fails to alert you to files that you forgot to git add
to copy into the index. So, having collected up the list of comparison results, Git also has to scan through the work-tree to find every file that’s actually there. Any file that is in the work-tree, but does not have a corresponding copy in the index, is untracked.
This scanning is one of the slower parts of git status
(though the actual speed depends a lot on the speed of your operating system). Git starts by reading through every file and sub-directory (“folder”) name within the top level work-tree directory. It has to read this directory entirely, of course, because it’s the top level of the work-tree. Any file name in this directory represents a file in the work-tree, and the file is either tracked—in the index—or not. If the file is untracked, Git will complain about it unless you also suppress the complaint, using a .gitignore
entry.
For directories found in the top level, though, git status
does not check whether the directory is in the index, because directories are never in the index. Instead, Git just looks inside the directory to see what files and directories it contains. Any files within that directory have to be checked to see if they’re untracked, and if so, whether git status
should gripe about them. If the directory contains its own sub-directories, those sub-directories must be scanned in the same way, recursively.
What listing a directory in .gitignore
does is give Git permission to not look inside it. Let’s consider the Debug/
directory specifically, for instance. Let’s assume that there are no tracked files inside Debug/
, and that you have run git status
.
Git will start by reading the top level directory. In this directory, it finds files named .gitignore
, 3dengine_headeronly.sln
, and CMakeLists.txt
. Those files are not in the index and not listed as a name or pattern in .gitignore
, so git status
will complain about them (at the end, when it lists untracked files). But it also finds .vs/
, 3dengine_headeronly/
, Debug/
, Tests/
, cmake-build-debug/
, packages/
, and resources/
.
Now, if there are some tracked files within (say) packages
, Git’s going to have to descend into packages
anyway, to compare the index copies of those files with their work-tree copies. But we’ve assumed here that there are no tracked files in Debug
. So git status
should be able to run through the .gitignore
entries and see that [Dd]ebug
(Git removes the trailing slash for comparison purposes) matches Debug
. This would permit git status
to skip over Debug
entirely, not reading it at all.
The fact that Debug/
comes out at the end, with just the trailing slash, means that Git must have opened and scanned the Debug
directory. The only reason Git would do that is if there is at least one tracked file inside Debug
. Hence:
git ls-files --stage | grep Debug/
will probably show at least one tracked file. Even so, it’s not clear why the [Dd]ebug/
rule did not match the untracked files contained in the directory, so getting a list of the exact names, and using git --no-index check-ignore -v
on any tracked files inside Debug/
, might be helpful.
(This optimization, where git status
sometimes doesn’t open a directory at all, is especially tricky, and is where a lot of .gitignore
issues start. There’s not much to be done about this until and unless someone teaches Git better ways to skip, or not skip, entire directories. However, using git status -uall
definitely helps some of the other tricky cases.)