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.
# 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!)
/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:
/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 intmp/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.
Top comments (0)