Skip to content

Instantly share code, notes, and snippets.

@sdondley
Last active August 5, 2024 19:43
Show Gist options
  • Save sdondley/f80c2b36fcf964dc0f9de53e147b694c to your computer and use it in GitHub Desktop.
Save sdondley/f80c2b36fcf964dc0f9de53e147b694c to your computer and use it in GitHub Desktop.
Guide to Gitignore

The Straightforward Guide to Gitignore Patterns

Introduction

Git's primary job is to hep us track changes to the files in our repository. Towards that end, Git dutifully and repeatedly informs us about untracked files to help remind us to add them with with a git add command.

But there are many files we don't need or want to track and wish to exclude from our repository. We want Git to ignore these files so Git will stop reminding us about their existence. This helps eliminate noise from Git's reports, allowing us to focus on more important matters. More significantly, ignored files will not be added to the repository when we issue a git add . command.

Examples of files we might want to ignore include:

  • those automatically generated by the OS
  • log files and build files
  • files containing sensitive information like passwords or API keys
  • configuration files that are specific to our development environment

Git provides us with the "gitignore" feature for ignoring such files. Think of it as a "Do Not Track" list for the files in your project. The primary way for adding files to the list is to create a text file named ".gitignore" in the root directory of the repository. Inside this file we add patterns, one per line. Git calculates which files to ignore from the patterns we add to the .gitignore file.

Note that while it's possible to create multiple .gitignore files in a repository, it's best practice to keep a single .gitignore file in the root directory to keep things simple. And so except where otherwise noted, this guide assumes you have precisely one .gitingore file and it is placed in the root directory of your repository. This will also help keep this guide focused on the topic at hand, gitingore patterns.

Using Patterns in the .gitignore File

The .gitignore file is just a series of text patterns, one pattern per line. Blank lines and comments (line beginning with the # sign) are ignored.

Let's now take a look at the most basic pattern possible:

foo

Though simple, this pattern packs quite a bit of hidden meaning. When intepreting ignore patterns, the first thing to look for is whether the pattern contains any forward slashes. Forward slashes change the meaning of a pattern drastically. This pattern has none but we will revisit the effect of slashes very shortly.

So which files will our slashless foo pattern tell Git to ignore?

First, it tells Git not to track any file or symlink with the name "foo" no matter which directory it's in. Second, Git ignores any file within any directory tree named "foo" anywhere in the repository.

So this simple pattern ignores quite a bit! Here's some concrete examples of paths ignored by the foo pattern:

Paths Matched by foo Match Explanation
./foo Any path, either file or directory, matching the name "foo" in the root directory
./bar/foo Any path matching "foo" in any subdirectory
./bar/foo/readme.md Any file inside a directory named "foo"
./bar/foo/buzz/fuzz/blah.txt Any file nested inside the directory tree named "foo"

It's helpful to reinforce two important points:

  • the foo pattern applies to both files and directories anywhere in our repository no matter how deeply nested
  • ignored directories cause any file anywhere inside of its directory tree to get ignored

So we can see patterns without slashes are pretty greedy patterns and gobble up many paths. This isn't always desirable and we may want Git to be more selective about which paths patterns should match. Slashes can help us do that.

Using Slashes to Make Patterns More Restrictive

Slashes change the meaning of patterns significantly and help us limit which paths they match. The location of slashes–whether at the the beginning, middle or end of a pattern–is also significant.

First, let's see how the pattern matches differently when adding a slash to the end of the humble foo pattern:

foo/

By adding a slash to the end of the pattern, we tell Git to only match paths that are directories. In other words, Git will no longer ignore files named foo. However, it still ignores all files inside the directory tree of a directory named foo. It also applies to any directory named foo no matter where it is in the repository.

But what if we only want to ignore the foo directory at the top level of our repository? We can turn again to the slash but this time we are going to add it to the beginning of the pattern:

/foo/

Now only the single 'foo' directory in the root of the repository and all the files within it are ignored. What happens if we remove the trailing slash now and leave the leading slash?

/foo

Our pattern becomes slightly more permissive and now ignores a file named "foo" if it's in the root of the respository in addition to a "foo" directory. These pattern demonstrate:

  • Slashes at the end of a pattern only match directories (ignoring all files in its directory tree)
  • Slashes at the beginning make patterns relative to the root of the repository

Finally, we can place slashes in the middle of our patterns. This has the same effect as a slash at the beginning of the pattern, making them relative to the root of the repository:

bar/foo/readme.txt

Now only the single readme.txt files in the foo directory in the bar directory in the root of the repository are ignored. Note that /bar/foo/readme.txt will behave in precisely the same way. If there are slashes in the middle of a pattern, we don't need one at the beginning.

So as we can see, slashes can help tame our patterns and make them match fewer files.

Using Path Wildcards in Patterns to Match More Paths

While slashes are helpful, they can often be a little too restrictive. So Git gives us the double asterisk ** wildcard to help us match many paths with a single pattern easily. Like with slashes, the meaning of the double asterisk changes depending on where we place them within a pattern.

When at the beginning of a pattern, ** matches all directories:

**/foo

This pattern matches all these example paths:

./foo
./bar/boo/foo
./bar/buzz/fuzz/foo

Note that **/foo behaves precisely the same as as the simpler pattern foo without the double asterisks.

When at the end of a pattern, ** matches all the files and directories inside of a directory relative to the root of the repository:

foo/**

This matches all files inside the foo directory tree no matter how deeply nested. Note that it does not match something like blah/foo because the slash in the pattern forces it to be relative to the root of our repository.

When the double asterisks are between slashes, they match zero or more directories:

foo/**/bar

This matches any path with 'bar' in it that is anywhere within a foo directory at the top level of the repository:

  • foo/bar matches * foo/buzz/fuzz/bar also matches * blah/foo/bar does not match

You can use multiple double asterisks in a pattern:

**/foo/**/bar

Now the 'blah/foo/bar' path will be ignored.

Character Wild Cards

Git supplies us with another set of tools, often called "wildcards" but more accurate to say "shell globs," to make matching files easier.

Let's say we want to ignore all log files which end with a 'log' file extension. It would be tedious to list the names of all these possible files. We can use the "*" wildcard character to help us.

The '*' wildcard behaves like the asterisk character in a shell glob, matching zero or more of any character except the '/' character. The following pattern matches 'foo.log' and 'bar.log' but does not match 'foo/bar.log':

*.log

A subtle but important point: although the slash in 'foo/bar.log' path prevents a match, Git still ignores the 'bar.log' file. This is because *.log pattern matches the file name blah.log which gets ignored no matter what subdirectory it is in.

By not matching slashes, the asterisk makes it possible to do something like:

foo/dir_*/*.log

This pattern ignores all log files in /foo in a directory startng with 'dir_'.

Git also provides us with the '?' widlcard character which matches exactly one character except '/':

foo.log.?

This pattern matches 'foo.log.1' and 'foo.log.2' but not 'foo.log.12' or 'foo.log./'.

Finally, the square brackets [] can be used to match any one of the characters inside a range:

foo.log.[a-z][0-9]

This pattern matches 'foo.log.a1' and 'foo.log.b2' but not 'foo.log.12'.

You can also do something like:

foo.log.[axz]

This pattern matches 'foo.log.a', 'foo.log.x', 'foo.log.z' but not 'foo.log.c'.

Negating Patterns

Another useful feature of .gitignore files is the ability to negate patterns by preceding a patteern with "!" (exclamation point). This tells Git to stop ignoring files if they match the pattern. For example:

*.log !important.log

Here, Git ignores all log files not named 'important.log'. Negation allows you to pull off some interesting tricks. Consider this group of patterns in a .gitignore file:

* !
*/
!*.gitignore
!*.md

Together these patterns tell Git to ignore everything except for directories and .gitignore files and any file with an '.md' extension. The first line tells Git to ignore all files and all directories. The second line tells Git to stop ignoring all directories. The third and fourth lines tell Git to not ignore the types of files we want to track.

Finer Points

Now that you know the basic gitignore tools at your disposal, let's cover some subtler points of gitignore patterns that may not be immediately obvious.

Ignoring a Directory is Different Than Ignoring the Files Inside It

There are different approaches to ignoring files in a directory. We can match the directory with a pattern like foo/ or foo . This tells Git to indirectly ignore everything in the directory tree of "foo" by ignoring the directory itself.

Alternatively, we can use a wildcard to directly ignore all the contents of foo with foo/* but not the "foo" directory itself. This is a subtle but important distinction.

The latter method provides us with more granular control over which files we ignore. For example, using the asterisk allows us to negate specific files in the directory:

foo/*
!foo/README.txt
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment