Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Jeremiah Lee
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Jeremiah Lee

Collocating My .gitignore Configuration Files With The Omitted Files

By
Published in Comments (9)

I believe that maintainable code is code that's easy to find and easy to delete. And, a big part of what make's code easy to find and easy to delete is collocation. Which is why I've been experimenting with collocating my CFML, JavaScript, and CSS files; as well as my collocating my ColdFusion partials with my CFML views. This morning, as I was setting up a new project, it occurred to me that my .gitignore files might present another opportunity for collocation.

In a git repository, the .gitignore file tells git which files to exclude from source control. For example, a common entry in this file is .DS_Store, which is a MacOS system file that gets automatically generated by the Finder app. And, if you need to compile files, it's common to add temporary directories like dist and .parcel-cache to the .gitignore.

Historically, I put one .gitignore file in my repository root; and then add deep file paths to it as needed. But, a git repository can contain any number of .gitignore files. And it occurs to me now that my project files might be easier to maintain if I use "local" .gitignore file with "local" file entries.

To illustrate what I mean, when I'm not using containers for deployment (with injected runtime secrets) my projects often have a /config directory that contains two files:

  • config.json
  • config.template.json

The config.json file contains secrets and should never be committed to source control. So, I create a sibling file, config.template.json, which codifies the shape of the configuration, but which omits any secret values.

To make sure that config.json never gets committed, I would normally go into my root .gitignore and add the entry:

config/config.json

But now the logic for this /config directory is spread across two different locations. Which feels unnecessarily complex, harder to maintain, and non-obvious while looking at the file system.

So, what I'm going to start doing—as much as it makes sense—is create small, localized .gitignore files that are collocated with the files that they manage. In this case, it means putting a .gitignore file directly in my /config directory that simply contains the one file entry:

config.json

My /config directory would then end up with this structure:

  • /config/.gitignore ← one entry for config.json
  • /config/config.json
  • /config/config.template.json

Now, when I'm looking at the /config directory, all of the code and the source control logic is collocated in one place. And, it becomes much more obvious that source control has caveats relating to this folder due to the existence of the .gitignore file.

Another potential "win" here is that the .gitignore file can contain comments. Which provides both an opportunity to omit files from source control and explain why they are being omitted.

As an example of this, one Pro Tip that I learned from Jamie Krug many years ago was to always have a /scribble folder in my web root that gives me a place to experiment with code that won't be committed to the project. And, historically, I've managed this folder by adding a scribble entry to the root .gitignore:

wwwroot/scribble/

But, the problem with this is that if a new developer clones the given repository, there's no scribble folder in their file system. Which means that every new developer has to be taught that they can create a scribble folder and put experimental code in there without having to worry about accidentally committed code to the repository.

A local .gitignore file, located withing the scribble folder, would solve this problem.

wwwroot/scribble/.gitignore

This local .gitignore will:

  1. Commit the folder to the repository without committing any of the files. Which means each new developer won't have to create it.

  2. Give me a place to document why the folder exists and how to use it.

For example, here's the .gitignore that I just created this morning during this thought experiment:

# The scribble folder is a place to put random scripts that don't get committed to the
# source control. This is a good place to experiment with ideas and to keep small
# utilities that are helpful to you but which are not helpful to others. That said, it
# runs as part of the application context and has access to the Injector.

# Ignore all files and folders in this directory.
*

# ... except for the gitignore file itself.
!.gitignore

Now, if a developer clones this git repository, they will get an empty scribble folder and some directions about why that folder exists. All in one place.

Over the years, one of my most common pain points in web development has been that files that "change together" don't always "live together". Which means that implementing one change means opening files both "here" and "over there". What I'm trying to do now is become more aware of the files that change together; and, see if I can move them closer to each other in the file system.

Want to use code from this post? Check out the license.

Reader Comments

142 Comments

I like your /scribble example, Ben. It's like .gitkeep except you are expecting the future files in that folder to NOT be committed to the repo, which .gitkeep wouldn't help you with. Slick.

15,939 Comments

@Jason,

I've definitely used the .gitkeep concept in the past, which I believe is just a usage pattern explored in "user land", not something officially part of git.

For anyone reading this that might not be familiar with the .gitkeep technique, it's important to understand that git can't commit empty folders, it can only commit files. So, if your repo needs to have an empty folder in place, developers can put a .gitkeep folder in there as a way for git to see it and track it (technically it's tracking the .gitkeep file, not the folder, but the file lives in the folder). There's nothing special about the .gitkeep file, it's just being used for its side-effects.

I've used this in the past to keep "processing" folders in the project definition. For example, we would sometimes designate a "scratch" folder at work where intermediary files could be written during processing workflows (such a ZIP or Image generation). And, if the folder didn't exist, ColdFusion commands would throw an error that the path doesn't exist. So, to cut down on the developer complaints (or having to hard-code directoryCreate() initialization scripts), we would just throw a .gitkeep in there.

This was before I realized that you could have multiple .gitignore files. Now, the secondary .gitignore files tend to be my first choice.

48 Comments

@Ben Nadel,

Regarding .gitkeep, since I hadn't heard of it I googled it, some folks (okay, StackOverflow) think a README might be a better choice, with explanations if not obvious. I would probably agree with that, since .gitkeep isn't "official", though you could put text in it, certainly?

15,939 Comments

@Will,

Dang it, a README is actually a really good idea!! You could even combine the two in places where you do want to exclude files. Basically just add the README to exclusion:

# Exclude everything.
*

# Except these:
.gitignore
README.md

That feels like the best of both worlds!

142 Comments

Yeah, I have definitely used README.md files in folders that are "there to be used" but not containing actual parts of the codebase. I think combining that with your /scribble approach might be a great solution in a number of different situations.

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel