gitignore exceptions: The Good, the Bad and the Ugly

As you know, gitignore files allow us to make ignore exceptions for files and directories using the the exclamation mark !. For multi-level directory structures adding gitignore exceptions can become really tricky.

Let’s take an example. Say we have the following structure on our local:

my-app/
  └── .gitignore
  └── dir1/
  └── foo.txt
  └── bar.txt
  └── baz.txt

We want to ignore everything in my-app directory except the file baz.txt. We simply add to our .gitignore:

  *
  !baz.txt

Easy, right? Pfff… 🀭

Β 

What if we have the following structure, instead:

my-app/
  └── .gitignore
  └── dir1/
        └── foo.txt
        └── bar.txt
  └── baz.txt

Now we want to ignore everything in dir1 except foo.txt.

- Ah, cmon…, that’s too easy, here, catch it:

  dir1
  !dir1/foo.txt

Surprise! It will not work. Git will still ignore everything inside the dir1 directory. Don’t believe me? Check it yourself:

gitignore exceptions - ex 2

As you can see, only baz.txt from the root folder will be added to the index.

Fortunately, the solution is pretty simple here:

  dir1/*
  !dir1/foo.txt
gitignore exceptions - ex 3

With an asterisk added, we say: Ignore everything in the folder. While, without an asterisk, we say: Ignore the folder.

When we are not dealing with gitignore exceptions, there’s no difference between using dir/* or just dir. This happens, because, if everything is ignored in the directory, then git will find no meaning in adding it to the index.

But things are changing when we have exceptions in gitignore. And the reason why it’s not working when we simply use dir is that:

It is not possible to re-include a file if a parent directory of that file is excluded.
Git doesn’t list excluded directories for performance reasons, so any patterns on contained files
have no effect, no matter where they are defined.
git-scm.com/docs/gitignore

Β 

- A-ha… I get it now. 😜

Are you sure? If you are, then let’s take a look at another example. This time we have the following structure:

my-app/
  └── .gitignore
  └── dir1/
        └── foo.txt
        └── dir2
              └── bar.txt
              └── baz.txt
  └── qux.txt

And now, we want to ignore everything in dir1 but keep the baz.txt inside dir2. So, in the end, we only want to have qux.txt and dir1/dir2/baz.txt in the index.

- Easy, here you go, boomer!

  dir1/*
  !dir1/dir2/baz.txt

Wait, mmm… this will not work because dir1/* will ignore the dir2, and since it’s ignored we can’t re-include baz.txt. Give me a minute… πŸ€” Maybe…

  dir1/dir2/*
  !dir1/dir2/baz.txt
  ( ... πŸ€” )
  dir1/* ... ( No, this will ignore the dir2 again... 😟 )

Ok, I give up. What can we do? You, paranoid! 🀬

__________

Don’t panic! It has a solution, even if it’s ugly. We need to add .gitignores inside dir1 and dir2.

my-app/
  └── .gitignore
  └── dir1/
        └── .gitignore
        └── foo.txt
        └── dir2
              └── .gitignore
              └── bar.txt
              └── baz.txt
  └── qux.txt

In .gitignore inside dir2 we need to have:

  *
  !baz.txt

And, in .gitignore inside dir1:

  *
  !dir2

Don’t forget to empty any directives you had for dir1 and dir2 in .gitignore in the root directory.

gitignore exceptions - ex 4 - illustrate

And, --dry-run again, to check πŸ™‚

gitignore exceptions - ex 4

This solution works, however, I don’t think using multiple gitignores in different tree levels is a good sign. I suggest doing the maximum on your behalf to avoid such situations. Cheers! 🍾

Β  Β