Anatomy of a .gitignore

Anatomy of a .gitignore

tl;dr?

In a hurry? Skip to the tl;dr.

When we work on a software project, we use Git as version control — or at least, I hope y'all do! Having a version control system (VCS from now on) allows us to have a history of the changes we can later audit, to mix our work with others', and to have an authoritative copy of the project's codebase.

But not everything should be stored into version control! Compilation artifacts, temporary files, and sensitive information such as API keys and keystores should all be kept out of Git. How does this work? As most folks know, Git uses .gitignore files to list out what should be kept out of version control.

What many folks don't know is what's in a .gitignore file, how to create one easily, what advanced features are available, and how they interact with other systems in Git and with each other. This post expands on a very old post of mine about how to manage the .idea folder in version control for the benefit of the whole team. You can go read that immediately if you wish, but I will provide a tl;dr and some more up-to-date recommendations here, so you don't really need to.

What do .gitignore files do?

The basic concept is simple, and you can read the comprehensive documentation… or you can just think of them this way: .gitignore files tell Git what files and folders it should pretend don't exist. Git will not track changes in those files and folders anymore, from the second you add them to the ignored list.

Note!

From now on, we'll also call .gitignore files ignore files, and use the terms exclusions and excludes as synonyms to (collections of) ignored items.

Confusingly, there are other mechanisms in Git that achieve similar results:

  • Global excludes — this is a global file in the same format as an ignore file, whose path is set in the core.excludesFile Git configuration)
  • Repo-wide excludes — an ignore file at the special path .git/info/exclude

You can have different .gitignore files in different folders around your repo, each taking effect for that directory and its children:

An example directory structure showing the influence of different .gitignore files

The closer an ignore statement is to a file it matches, the higher priority it has. This means that, for a file in the app/src folder in the example image above, the red .gitignore takes precedence over the blue one, which in turn takes precedence over any repo-wide, and then global, exclusions. If there are no conflicts between different sources, the effects of all ignores are cumulative as you get to narrower scopes.

The .gitignore file format

Every ignore file contains a list of patterns that will be used to determine which files Git should ignore. The effect of these patterns is additive, that is, the final effect of each file is the sum of each line's effects.

Each line, or pattern, can be of several types. The most common ones are:

  1. Empty lines and comments — these do nothing
  2. An exact file or folder path that can be absolute or relative
  3. A glob pattern, matching multiple files, folders, or file types

While these look simple, there are some caveats to keep in mind. First of all, the path separator is / regardless of the OS you're using; this may feel odd on Windows, but it enables portability.

If a pattern starts with the / character, or contains it anywhere but at the end (such as mydir/somefile.txt) then the paths will be relative to the ignores file. Otherwise, they'll apply to all siblings and their children (e.g., *.apk applies to all files with an .apk extension, in any folder, since it contains no /).

You can use ** globs to indicate "any path", which can be substituted by any number of directories at evaluation time, including by no directories at all. A pattern starting with **/ essentially means anywhere, and it's used to match directories that don't live only in one place relative to the ignores file. In the above example, **/test/reports will match a folder called reports inside of a folder called test, at any point in the hierarchy underlying the ignores file.

An ignore file, explained

A very simple .gitignore file can contain entries like:

## Seb's super basic ignores example file

# 1. Exclude dirs named 'build'
build/

# 2. Exclude all files in the sibling folder called `.gradle`
.gradle/**

# 3. Exclude 'secretlib.jar' in sibling 'libs' dir
libs/secretlib.jar

# 4. Exclude dirs named 'reports' inside of 'test' dirs, anywhere
**/test/reports/

# 5. Exclude APK files
*.apk

# 6. Exclude 'local.properties' sibling
/local.properties
The effect of each rule on a sample directory structure

Each line impacts different entries in our example folder structure (expanded from the previous example). Rule #1 impacts more than one folder, since it doesn't start with or contain a / except at the end, meaning it's applied deeper into the structure; rule #4 would also apply to multiple folders, as long as their paths end with /test/reports, but we only have one such folder matching. In this case, we used the ** glob to say "somewhere in the hierarchy, doesn't matter where".

Rule #2 would apply to any files in the .gradle folder (the pattern contains a / so it is referring to a sibling folder, not to ones anywhere else), even if we only have one in our example. Rule #5 only specifies an extension and contains no /, so it applies to any APK files anywhere in the hierarchy. By contrast, rule #6 explicitly starts with a /, anchoring its matches to only appear as siblings of the ignore file.

Rule #3 can only match a libs/secretlib.jar path that starts in the same folder as the ignore file; by now, you should be familiar enough with the rules to see that is caused by the / in the middle of the pattern.

All rules have their exceptions

We've so far seen how to add items to an ignores list… but what if we have a directory we want to exclude everything in, except one or more specific files? Luckily, there is a way to do it, by negating patterns with the ! prefix (like we'd do in code):

# 1. Ignore an entire directory
/dir-to-ignore

# 2. And then un-ignore only specific files inside
!dir-to-ignore/readme.md
!dir-to-ignore/docs/setup.md

Ignores in practice

In real life, it's hard to write good ignores, and easy to mess them up. As such, I recommend using something like the .ignores plugin for IntelliJ IDEA and Android Studio. The plugin provides an ignores file generator, which has a ton of templates (courtesy of GitHub) and can make the process a true breeze:

The "Ignore File Generator" dialog

Not only that, but the plugin also augments the IDE's built-in capabilities by providing syntax highlighting, autocompletion, and inspections, making editing and maintaining .gitignore files a much safer and intuitive task:

Autocompletion in a gitignore file thanks to the .ignore plugin

A few folks, when reviewing the draft for this post, also mentioned gitignore.io; while it does work, I find having the feature directly in the IDE much more useful as it saves me from copy-pasting stuff around. But if you prefer a website, that's a valid option and functionally equivalent, although you'll miss out on the other features the plugin offers.

What should be ignored

This is one of those questions whose answer is a resounding it depends!, but there's some stuff you always want to ignore, regardless of your project:

  • Build outputs
  • Temporary files
  • Secrets and keystores
  • Generated files (e.g., sources created by annotation processing)
  • OS files
  • Local settings, pointing to paths on your machine

The templates provided by the .ignore plugin luckily cover all these, so you just need to pick the ones that apply to your project (e.g., Gradle, Android, IntelliJ IDEA) and clean up any duplicates that may appear in the file.

What about IDE settings?

The project IDE settings are stored in the .idea folder, at the root of the project folder structure. There are different schools of thought on whether this folder should be excluded from VCS or not. Anecdotally, those who ignore the entirety of the folder are the majority, but they are also arguably doing it wrong.

The official guidance lists a few files that you want to avoid sharing in VCS, but I don't think the best approach is to only ignore these files. My recommendation is to ignore the whole folder, like the JetBrains template in the GitHub collection — and thus, the .ignore plugin — does, but to make sure you exclude some key parts. This allows folks in the team to have arbitrary plugins installed, without the hassle of their settings files constantly popping up as untracked.

My .gitignores always contain this list of exceptions:

# IDEA/Android Studio project settings ignore exceptions
!.idea/codeInsightSettings.xml
!.idea/codeStyles/
!.idea/copyright/
!.idea/dataSources.xml
!.idea/detekt.xml
!.idea/encodings.xml
!.idea/externalDependencies.xml
!.idea/fileTemplates/
!.idea/icon.svg
!.idea/inspectionProfiles/
!.idea/runConfigurations/
!.idea/scopes/
!.idea/vcs.xml

Why am I adding those exclusions? Simply put, those are essential for important IDE features, and enable better team cooperation. The ones that are useful to everyone are:

  • The codeStyles directory contains the settings for the project code style; making sure the whole team shares the same style means the IDE code formatter will behave consistently for everyone, and there will be fewer "reformat this" PR comments (and static analysis warnings). You need to make sure you're using a scheme stored in the project, and not globally in the IDE:
The Editor > Code Style scheme selector, showing the Project scheme as selected.
  • The inspectionProfiles folder contains the project inspections profile; again, you want the entire team to operate on the same rules, and that you're using a project-scoped inspections profile:
The Editor > Inspections profile picker, showing the Project Default item as selected
  • The runConfigurations folder contains, unsurprisingly, the run configurations for the project. Note that you need to explicitly mark run configurations as shared for them to be stored here:
The "store as project file" option for a run configuration
  • The vcs.xml file tells the IDE that the project is a Git repository, and some metadata about it

The other entries are there in case those files/folders exist, but they don't always do:

  • The custom shared scopes you may have defined in the project (useful for many things: searching, limiting inspection scopes, etc)
  • The copyright information to put in the source files
  • Any custom fileTemplates that you've set up
  • The dataSources.xml file contains the configured data sources (IJ Ultimate only — the database connections used for the Database features)
  • If you use the Detekt plugin, then you will likely want to share its configuration with the rest of the team: that's the detekt.xml file
  • The encodings.xml is only created if you explicitly set the encoding for some files; it is thus a rare sight, as the IDE is pretty good at guessing automatically
  • If your project requires a certain plugin to work, then you can use the Required plugins feature, whose settings are stored in externalDependencies.xml
  • There are several things on your classpath that you never use, so you may want to exclude from auto-import and from the code completion. This can be particularly useful if you use Jetpack Compose. Those exclusions are saved in the codeInsightSettings.xml file.
  • Lastly, did you know you can set a project icon that will show in the Welcome screen and in Toolbox? On the Welcome screen, right click a project, select Change Project Icon…, and pick an svg file. It will be saved as .idea/icon.svg — and of course you want to store it in Git.
A screenshot of the "Change Project Icon" dialog

Ok, tl;dr?

If this article has a lot of info you don't need or care about, but you still want to reap the benefits, then:

  1. Plop down my uber-.gitignore file in the root of your project(s)
  2. Review any .gitignore files you may have in your Git repositories, consolidating them as much as possible into the root ignores file
  3. Delete all redundant ignore files
  4. Make sure your IDE settings are using project-scoped profiles
  5. Commit and push!

Thanks to Mark Allison, Rui Teixeira, Daniele Bonaldo, Vincenzo Costagiola, Hans-Petter Eide, cketti, and Pablo Gonzalez Alonso for proofreading ❤️