gitignore exceptions: The Tricky Parts

Sometimes, when pushing our changes to remote, we want to ignore everything in the directory except some files. In .gitignore we must use the exclamation mark ! to do that.

Β 

Let’s say we have the following structure in our local:

my-app/
  └── .gitignore
  └── dir1/
  └── file1
  └── file2
  └── file3

And, we want to, for example, ignore everything in my-app except file3. We simply add to our .gitignore:

  *
  !file3

Easy, right? Pfff… 🀭

Β 

Let’s take another example. This time we have the following structure:

my-app/
  └── .gitignore
  └── dir1/
        └── file1
        └── file2
  └── file3

And, we want to ignore everything in dir1 except file1.

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

  dir1
  !dir1/file1

πŸ₯ Padam! And… I don’t want to disappoint you but this will not work. Don’t believe me? Check it yourself:

gitignore exceptions - ex 2

As you can see, only file3 from the root folder will be added to commit, above.

Fortunately, the solution is pretty simple here:

  dir1/*
  !dir1/file1
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 commit.

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 the final example. This time we have the following structure:

my-app/
  └── .gitignore
  └── dir1/
        └── file1
        └── dir2
              └── file2
              └── file3
  └── file4

And now, we want to ignore everything in dir1 but keep the file3 inside dir2. So, in the end, we only want to have file4 and dir1/dir2/file3 in the commit.

- Easy, here you go, boomer!

  dir1/*
  !dir1/dir2/file3

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

  dir1/dir2/*
  !dir1/dir2/file3
  ( ... πŸ€” )
  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
        └── file1
        └── dir2
              └── .gitignore
              └── file2
              └── file3
  └── file4

In .gitignore inside dir2 we need to have:

  *
  !file3 

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

Although this solution works fine, I don’t think using multiple .gitignores in different tree levels is a good sign. I suggest doing as maximum as you can to avoid this kind of situation. Cheers! 🍾

Β  Β 

Icons made by DinosoftLabs and smalllikeart from flaticon.com.