Narnach's blog

The thoughts of Wes "Narnach" Oldenbeuving

.gitignore does not unignore my file!

Today I ran into an interesting edge case that my colleague and I could not easily explain until we checked the git manual. I’m sharing the details because even after using git for 15 years, I was mildly surprised by the behavior. Luckily, it does make sense now that I understand the underlying reasons.

What’s .gitignore?

Anyone using git for version control is likely familiar with the .gitignore file where you can specify which file name patterns git will ignore by default when you perform various actions, such as git status, git diff and git add. It’s very useful to ignore generated files and temporary files. You can add a line that starts with a ! to make it NOT ignore files with that pattern. Patterns are evaluated top to bottom, and the last pattern that matches a file will determine if it’s ignored or not.

What we did

We wanted to add a simple .gitkeep file to preserve the tmp/pids directory in our project on all machines that checked out the project. This arguably trivial operation did not work and took me a while to figure out why it did not work.

1
2
3
4
5
# Lets git NOT ignore .gitkeep files, i.e. always check them in:
echo "!.gitkeep" >> .gitignore
touch tmp/pids/.gitkeep
git status
# ... tmp/pids/.gitkeep is NOT listed as new file

Why it did not work

So why is the file we just told git we want to NOT ignore still getting ignored? It’s because our .gitignore already contained this handy line that ignored all temporary files (because they have no place being checked in!)

1
/tmp/*

This line makes git ignores all files and directories inside tmp. It’s also the direct reason why our earlier change did not give the result we want. After some reading of the manual, I found this relevant bit (emphasis mine):

An optional prefix “!” which negates the pattern; any matching file excluded by a previous pattern will become included again. 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.

It really makes sense from a performance perspective to not recurse into children of ignored directories to see if they happen to be not ignored.

TL;DR: you need to re-include an ignored parent directory if you want to customize rules for its contents.

How to make git really NOT ignore your file

Let’s update .gitignore using our new knowledge. The git diff after my changes:

1
2
3
4
 /tmp/*
+!/tmp/pids
+/tmp/pids/*.pid
+!.gitkeep

The order of operations here is:

  • Ignore /tmp/* and all its children
  • Add /tmp/pids back
  • Ignore all .pid files in tmp/pids (because those are the only ones generated there)
  • Globally enable .gitkeep

Alternative: use the –force

Alternatively, you can use git add --force tmp/pids/.gitkeep to add it while ignoring .gitignore rules. Arguably that’s faster than checking the manual and thinking about it, but it also prevents you from learning why it failed to work in the first place.

This worked for me ™, so I hope it helps you as well. If you run into issues, feel free to reach out to me via Twitter or email. I’m an experienced Ruby software developer with a focus on back-end systems and an obsession with code quality. I even have a few Ruby on Rails maintenance services that I offer. If you still need to upgrade to Rails 6, then grab the handy free checklist or reach out to have me do it for you.