There's not enough information in your question to be absolutely sure of this (e.g., you might have submodules), but most likely the problem is the usual one with .gitignore
directives that is covered by this sentence (which, apparently, many people find confusing) from the documentation:
It is not possible to re-include a file if a parent directory of that
file is excluded.
The way to understand this sentence is to realize that Git follows a simple algorithm when scanning a working tree's files and directories (folders):
- Read some level of the working tree (starting with the top, but we'll come back to step 1 below).
- For each file or directory name found here, e.g.,
README.txt
, .git
, file.ext
, main.py
, main.pyc
, subdir
, and so on, execute the "check ignore" algorithm.
-
For files:
If the file isn't ignored, and isn't in Git's index right now, gripe that the file is untracked (git status
) or go ahead and add it (git add
with any en-masse style add that adds everything and sees this file).
If the file is ignored and isn't in Git's index right now, be quiet (git status
: shut up about untracked-ness) or do nothing (git add
: ignore it).
If the file is ignored but is in Git's index right now, it's not really ignored: check its status (git status
) or add it (git add
).
For directories (folders):
If it's ignored, don't even read it. If it's not ignored, read it and execute the three rules on its contents.
(Any .git
directory containing a repository is always either ignored entirely or converted to a submodule gitlink as appropriate. You cannot force a .git
to be included in a repository: Git absolutely refuses to nest repositories.1)
Since rule 3b handles ignored directories by not looking inside them, if Git ever reaches:
/MySolution/src/MyProject1/SqlServerTypes/
and, say, SqlServerTypes
itself is ignored, Git won't bother to read the /MySolution/src/MyProject1/SqlServerTypes/
directory.
Now, looking at https://github.com/github/gitignore/blob/main/VisualStudio.gitignore (which you should not just refer to—you should include any important lines from this in your question) I find that lines 24 and 25 read:
x64/
x86/
The trailing slash here tells Git that x64
and x86
should be ignored when they're directories, so Git won't bother to look inside /MySolution/src/MyProject1/SqlServerTypes/x64
. You attempted to override this with:
!SqlServerTypes/*
Unfortunately for you, this line means:2
!/SqlServerTypes/*
and not:
!**/SqlserverTypes/*
What you might want here is:
!**/SqlServerTypes/x64/
for instance, so that Git will look inside /MySolution/src/MyProject1/SqlServerTypes/x64/
.
Alternatively, if it doesn't include too much, you can simply list !x64/
and/or !x86/
to override the rules from lines 24 and 25.
1The point of refusing nested repositories is security. Early on, Git accidentally allowed people to store a .GIT/
or .Git/
, which was not a security hole on typical Linux setups which are case sensitive, but was a security hole on typical Windows and macOS setups which are not. Git has since been smartened-up to reject .git
, .gIT
, and so on, regardless of upper/lower-case mix.
2The reason for this is a bit complicated. Some .gitignore
lines are applied to every name component and some are applied to the full path name achieved so far in the scanning process outlined above. The full-path-name restriction applies if:
- the name in
.gitignore
starts with a slash, or
- the name in
.gitignore
contains a slash after removing a single trailing slash if present.
That second rule is the one biting you here. The "after removing a single trailing slash if present" is simply because .gitignore
entries can be marked as "applies only to a directory name" by appending a slash:
foo/
means do ignore anything whose last name component is foo
and is a directory, while:
foo
means do ignore anything whose last name component is foo
, directory or not. So that last slash gets stripped. But:
foo/bar
or:
foo/bar/
still contains at least one slash after stripping any trailing slash, so it's treated the same as:
/foo/bar
or:
/foo/bar/
Since .gitignore
files can appear at any level within the working tree, these "anchored" rules, as I call them, don't necessarily apply only at the top level. Instead, they apply at the level at which the .gitignore
was found. For instance, a working tree containing subdir
and subdir/.gitignore
at its top level, where subdir/.gitignore
lists foo/bar
, ignores /subdir/foo/bar/baz
if that exists, but not /foo/bar
or /foo/bar/baz
if those exist.
Note that the scanning process here is heavily involved in this whole directory / file distinction that OSes force upon Git. However, after an en-masse git add .
—or in fact, any git add
—only files, not any directories, appear in Git's index. You cannot get a directory into Git's index, no matter what you do here.3 And, since Git makes the next commit from whatever is in its index, the absence of directories in Git's index means that no Git commit ever contains anything except files.
3There's one trick that sort-of-works, except that the empty directories eventually turn into gitlinks, which act as half-assed submodules.4 Meanwhile, submodules—which use gitlinks, or index entries with mode 160000
—also allow you to store an empty directory in a repository, sort of: the empty directory may wind up with a .git
subdirectory. See How can I add a blank directory to a Git repository?
4The gitlink provides the path; a .gitmodules
file completes the submodule, and without a .gitmodules
file, you end up with a broken submodule, or as I sometimes call it, a half-assed submodule. By fully-assing-up the submodule, we fix that problem, only to find a .git
present after running git submodule update --init
or using recursive clone. So you might as well just create an empty .gitignore
or other dot-file.