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:
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:
- Empty lines and comments — these do nothing
- An exact file or folder path that can be absolute or relative
- 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' anywhere
# Note that you may need to exclude by hand other folders
# named build if necessary (e.g., in src/)
**/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
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:
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:
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 .gitignore
s 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
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
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
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.
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:
- Plop down my uber-
.gitignore
file in the root of your project(s) - Review any
.gitignore
files you may have in your Git repositories, consolidating them as much as possible into the root ignores file - Delete all redundant ignore files
- Make sure your IDE settings are using project-scoped profiles
- Commit and push!
Thanks to Mark Allison, Rui Teixeira, Daniele Bonaldo, Vincenzo Costagiola, Hans-Petter Eide, cketti, Pablo Gonzalez Alonso, and Eugen Martynov for proofreading ❤️