Home > Software design >  Git failing to ignore files in subdirectory
Git failing to ignore files in subdirectory

Time:09-20

My .gitignore file does not seem to be ignoring all the files I want it to.

================== Context ==================

In my git repo, I have several subdirectories, similar to this reduced example:

git base folder
    > .gitignore
    > Folder A
    > Folder B
        > Subfolder B0
            >SubSubfolder B0a
                > Files
        > Subfolder B1
            > SubSubfolder B1a
                > Files
            > File B1b.ft0
            > File B1c.ft0
            > File B1d.ft1
            > File B1e.ft2
            > Etc

I want to ignore all files in Subfolder B1 except (say) file B1b.ft0 and file B1e.ft2. I also want to ignore all files in SubSubfolder B1a and B0a. In my .gitignore file (there is only one), I have these lines:

#Ignore:
B1/**
B1a/
B0a/

#Include
!B1/B1b.ft0
!B1/B1e.ft2

However, all of the files in Subfolder B1 are included. Files in SubSubfolder B1a are ignored, however files in SubSubfolder B0a are not.

============== Attempted Solutions ==============

I decided to start by solving the problem of unignored files in Subfolder B1 first:
I tried B1/ in the .gitignore file, but this ignored all the files there, including the ones I want to keep.

I also tried B1/* and B1/*.*, but these both fail to ignore the other files in folder B1.

Then I tried manually listing all the files I want to ignore/keep in this folder. This worked, but there are a lot of files and they might change. I don't want to have to use this option.

================== Methods ==================

When testing if each option works, I used git check-ignore -v <filename> and git status --ignore. I also used git rm --cached -r . followed by git add -A to clear the repo of ignored files. (Subquestion: do I need to commit the .gitignore before it will take effect?)

================== Summary ==================

I'm getting more and more confused and annoyed by this, can anyone help me? (Do I just need to apply a generous quantity of *'s all over the place for it to automagically start working, as in this question?)

Please also explain why, and how, your solution works while my ones don't (if you know).

CodePudding user response:

After a significant amount of testing, reading documentation (quite good), and asking other people, I finally found a solution. (And yes it does involve sprinkling a generous quantity of *'s everywhere). I'll do my best to explain what I think was going on, and how it can be avoided.


Short answer:

Git was not ignoring the files in Subfolder B1 because it expected B1 to be in the root directory. Changing the .gitignore entries to **/B1/**, !**/B1/B1b.ft0, and !**/B1/B1e.ft2 fixed the problem.

Long(er) answer:

The key to understanding this problem is to understand some oddities of the .gitignore syntax. Fortunately, there is not much of it to learn, even though some of it is frustratingly arcane - for instance, you may know that gitignore files are read downwards, leading to this behaviour:

foo is not ignored foo is ignored completely
 foo 
!foo
 !foo 
foo

My issue was related to another arcane subtlety in the .gitignore syntax: how it parses filepaths. The gitignore directory separator is /, but depending on its location in a line, or how many of them there are, it behaves in quite different ways:

For entries with only one /:

/ at end of line, as in foo/ / in middle of line, as in foo/bar / at start of line, as in /foo
Git ignores every directory named foo, no matter how many subdirectories deep it is. Git ignores any folders and files named bar in the filepath <.gitignore_directory>/foo/. Folders (or files) named foo/bar will only be ignored if they have that specific filepath, i.e, the path starts in the same directory as the .gitignore file. Git ignores any files and folders named foo, but only if they are in the same directory as the .gitignore file.

For entries with multiple /s, the rules are a combination of the above rules. A few examples demonstrate:

/foo/ foo/bar/ /foo/bar
This pattern matches only one item: a subfolder in the same folder as the .gitignore, named foo. (Of course, everything inside it is ignored too.) This pattern also only matches one item, a subsubfolder named bar found inside the folder foo, which itself must be a subfolder of the folder that also contains the .gitignore file. This pattern is identical to one in the middle example, except it can also ignore a file named bar, not just a directory.

In all the above cases, files and subdirectories within any ignored directory foo can not be unignored by e.g. !foo/bar.

The devious subtlety is that a / at the end of a line like foo/ will cause any directory named foo to be ignored, no matter of its depth, but putting any character (including wildcards!) after that /, or putting any other / in the same line will mean it matches only relative to the path where the .gitignore is found. So then, how can files inside a subdirectory be unignored?

Wildcards

The fix is to make better use of the wildcards. There are three wildcards in .gitignore syntax:

  • ? - matches 1 or more characters, except /. Almost only used for filename matching.
  • * - matches 0 or more characters, except /
  • ** - matches 0 or more characters, including /.

? or * can be used to ignore all files in a directory, but not subfolders. The following examples will both ignore all files in the subdirectory foo, if foo resides in the same directory as .gitignore.

  • foo/*
  • foo/? This one will not ignore files with 0-character names.

However, note that these will not ignore files in e.g. <.gitignore_directory>/filepath/foo.

Since ** matches directories, it can solve the above problem like so: **/foo/. Even though there is a non-whitespace character after the /, this will ignore all directories and subdirectories named foo, because **/ matches all filepaths.

  • **/foo/? Ignores all named files inside any directory foo.
  • foo/**/bar.txt Ignores every file named bar.txt at any depth inside a folder called foo, where foo is in the <.gitignore_directory>.
  • **/foo/** Ignores everything inside every directory foo, but does not ignore the directory itself, and therefore I can unignore a select few files inside it!

Yay! My question is answered - almost.

Regarding SubSubfolder B0a:

For the entry in my .gitignore to ignore SubSubfolder B0a, I actually had something slightly different from what I asked. In my actual project, B0a is called .generated_files. I was not entirely sure what git would think of a .gitignore entry starting with a dot, so I had prefixed it with a slash, like so: /.generated_files/. (I forgot about this when I wrote the question.) As I have now learnt, this caused git to expect it only in the root directory, and therefore miss it. I also learnt that git does not care in the slightest if an entry begins with a ..


Final notes

Although It was not causing a problem in my case, I also discovered that an entry in a .gitignore file will match both files and folders. So if you had a file named foo and a folder named foo, putting foo in the .gitignore would cause them both to be ignored.

Hopefully this answer was helpful, I'm afraid it is so long that reading the official documentation might actually be easier to understand.

  • Related