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.
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.
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.
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.
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'.
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.
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.
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