Ignoring files with .gitignore
Published by marco on
Introduction
This article defines concepts like repository and working tree and then discusses how you can use .gitignore
files to determine the files and folders that Git considers during operations.
Concepts
From a command line, you can run git init
in any folder to make any folder a Git repository. Doing so creates a .git
folder with a database and configuration files for the local repository. Git considers any folder that contains a .git
folder with these configuration and database files in it to be a Git repository.
The working tree of such a folder is all files and subfolders in that folder other than the .git
folder.
Any non-trivial repository will contain at least some files that should not be committed to the Git repository. Commonly, these are build artifacts produced from the source files that are included in the repository.
.gitignore
A repository may include multiple .gitignore
files. From the official documentation]:
“Patterns read from a .gitignore file in the same directory as the path, or in any parent directory (up to the top-level of the working tree), with patterns in the higher level files being overridden by those in lower level files down to the directory containing the file. These patterns match relative to the location of the.gitignore
file.”
Git determines whether a given file is included in the working tree by collecting the patterns from any .gitignore
file anywhere in that file’s path. Patterns defined “closer” to the file override those defined “farther” away.
For example,
[Root]
π .gitignore
π ProductMedia
π Win10
π Content
π .gitignore
π Deploy
π Control
π .gitignore
π readme.txt
Git determines whether readme.txt
is included in the working tree by collecting the rules from files in the following order:
.gitignore
ProductMedia/Win10/Content/.gitignore
ProductMedia/Win10/Content/Deploy/Control/.gitignore
Patterns
A .gitignore
file contains zero or more patterns.
- Each pattern excludes the affected files from the working tree
- Each pattern affects only files in the sub-tree defined by the sub-folder in which the pattern is declared.
- Each pattern is interpreted relative to its containing folder (i.e., the root folder for a pattern is its declaration folder).
The following examples illustrate common patterns.
The column “Disables subsequent patterns” indicates whether subsequent patterns that target files in the ignored sub-tree will have an effect. As detailed below, the presence of a trailing *
determines whether Git considers subsequent rules. This only becomes relevant for re-including deeply nested files.
obj/
All files in any folder named
obj
anywhere in the sub-tree of the current folder or any sub-folder thereof.Disables subsequent pattern: Yes
/obj/
All files in the root folder named
obj
or any sub-folder thereof.Disables subsequent pattern: Yes
/obj/*
All files in the root folder named
obj
or any sub-folder thereof.Disables subsequent pattern: No
*.bin
All files ending in
.bin
anywhere in the sub-tree of the current folder.Disables subsequent pattern: Yes, but irrelevant
src/*/out/bin
All files in any path named
out/bin
found in any single sub-folder of any folder namedsrc
anywhere in the sub-tree of the current folder.Disables subsequent pattern: Yes
src/**/out/bin
All files in any path named
out/bin
found anywhere in any combination of sub-folders of the sub-tree of any folder namedsrc
anywhere in the sub-tree of the current folder.Disables subsequent pattern: Yes
Re-including files
You can also re-include files with the !
operator. For example, you might want to exclude everything in a folder but a single configuration file.
The following example ignores everything in the /out
folder except for the file settings.json
.
/out
!/out/settings.json
Difference between /
and /*
As very nicely explained in the answer on StackOverflow .gitignore exclude folder but include specific subfolder,
- The pattern
dir/
excludes a directory nameddir
and (implicitly) everything under it. Withdir/
, Git will never look at anything underdir
, and thus will never apply any of the “un-exclude” patterns to anything underdir
.- The pattern
dir/*
says nothing aboutdir
itself; it just excludes everything underdir
. Withdir/*
, Git will process the direct contents ofdir
, giving other patterns a chance to “un-exclude” some bit of the content (!dir/sub/
).
For example, the following patterns ignore all files in obj
and bin
folders but files named readme.md
in those folders should be included. The following patterns achieve this for obj
but not for bin
folders (because the missing *
prevents Git from considering the rule re-including readme.md
for bin
folders).
/obj/*
!/obj/Readme.md
/bin/
!/bin/Readme.md
This distinction is highly relevant for the next section.
Re-including deeply nested files
Suppose we have a folder structure as shown below.
[Root]
π Folder1
π Folder2
π Folder3
π ProductMedia
π Win10
π Content
π Other filesβ¦
π Deploy
π Control
π CustomerSettings.ini
π Bootstrap.ini
π Settings.xml
π Other filesβ¦
π Folder1
π Folder2
π Folder3
π Other foldersβ¦
π Win11
π Content
π Other filesβ¦
π Deploy
π Control
π CustomerSettings.ini
π Bootstrap.ini
π Settings.xml
π Other filesβ¦
π Folder1
π Folder2
π Folder3
π Other foldersβ¦
π Folder1
π Folder2
π Folder3
π Other foldersβ¦
We want the patterns that will ignore everything in any files in the Content
folder of any sub-folder of ProductMedia
except for the CustomerSettings.ini
, Bootstrap.ini
, and Settings.xml
files under the path Content/Deploy/Control/
of any sub-folder of ProductMedia/
.
You might think that the following would do the trick:
# Ignore everything in the "Content" folder of any subfolder of "ProductMedia"
ProductMedia/*/Content/*
# Except for the specified files under the path "Content/Deploy/Control/"
!ProductMedia/*/Content/Deploy/Control/CustomerSettings.ini
!ProductMedia/*/Content/Deploy/Control/Bootstrap.ini
!ProductMedia/*/Content/Deploy/Control/Settings.xml
Microsoft Copilot certainly thought so. This does not have the intended effect.
The pattern correctly uses the *
to indicate that Git should continue processing patterns for sub-trees of the excluded folder. It also correctly ensures that the rule applies both to Win10
and Win11
folders by using a *
.
However, while patterns affecting the path Content
will be considered, those affecting Content/Deploy
or any sub-folder thereof will not.
The trick, as outlined in the StackOverflow answer, is to re-include, then exclude each individual sub-folder in the path, as shown below.
# We ignore the generated "Content" subfolders in the ProductMedia folder
ProductMedia/**/Content/* # Allow subsequent processing
# Except for certain configuration files
!ProductMedia/**/Content/Deploy/ # Include "Deploy", allowing subsequent processing
ProductMedia/**/Content/Deploy/* # Ignore everything in "Deploy"
!ProductMedia/**/Content/Deploy/Control/ # Include "Content", allowing subsequent processing
ProductMedia/**/Content/Deploy/Control/* # Ignore everything in "Content"
# Finally, include the desired individual files
!ProductMedia/**/Content/Deploy/Control/CustomSettings.ini
!ProductMedia/**/Content/Deploy/Control/Bootstrap.ini
!ProductMedia/**/Content/Deploy/Control/Settings.xml
This pattern of re-including, then re-excluding each sub-folder suffices to allow Git to consider the file patterns at the end, while still ignoring all other files in any sub-trees that would otherwise have been included.