Ignoring Files in Git with .gitignore¶
Every Git repository accumulates files that should never be committed: compiled binaries, dependency directories, editor configuration, operating system metadata, and local environment files that contain secrets. Without a mechanism to exclude them, every git status output and git add . command becomes a manual filtering exercise. The .gitignore file is Git's built-in solution to that problem, and understanding how it works end to end, including its pattern syntax, scope model, and debugging tools, eliminates a class of frustration that affects developers at every experience level.
What .gitignore Does¶
A .gitignore file tells Git to leave certain files untracked. When a file matches a pattern in .gitignore, Git behaves as if the file does not exist for the purposes of git status, git add, and git commit. The file remains on disk; it is simply invisible to Git unless you explicitly force it with git add -f.
The key constraint is that .gitignore only affects untracked files. If a file is already tracked in Git's index, adding it to .gitignore has no effect on it. To stop tracking a file that was previously committed, you need to remove it from the index first:
After that, the file will be ignored as expected.
Pattern Syntax¶
.gitignore uses a glob-based pattern syntax. Each line in the file is one pattern. Blank lines are ignored, and lines that begin with # are comments.
Wildcards and Matching Rules¶
| Pattern | Matches |
|---|---|
*.log | Any file ending in .log in any directory |
build/ | The directory named build and all of its contents |
build | Any file or directory named build |
**/logs | A file or directory named logs anywhere in the tree |
**/logs/*.log | Any .log file inside any logs directory in the tree |
logs/** | Everything inside logs/, but not logs/ itself |
doc/*.txt | .txt files directly inside doc/, not in subdirectories |
doc/**/*.txt | .txt files in doc/ or any of its subdirectories |
?atch | Files with any single character before atch, for example patch or catch |
[abc].txt | Files named a.txt, b.txt, or c.txt |
[0-9].txt | Files named 0.txt through 9.txt |
A trailing / means the pattern only matches directories. A leading / anchors the pattern to the root of the repository, preventing it from matching in subdirectories.
# Only matches build/ at the repository root
/build/
# Matches any directory named build anywhere in the tree
build/
Negation¶
A pattern that begins with ! negates a previous rule. This is the mechanism for exceptions: ignore a broad category, then re-include specific items.
One constraint applies: you cannot re-include a file if its parent directory is ignored. Git stops descending into ignored directories entirely, so a negation pattern for a file inside a directory that is itself ignored will have no effect.
# This does NOT work: build/ is ignored entirely,
# so nothing inside it can be re-included
build/
!build/output.txt
To re-include specific files inside a directory, ignore the contents rather than the directory itself:
Comments and Blank Lines¶
# This is a comment. Git ignores this line entirely.
# The blank line above is also ignored.
*.log # Inline comments are NOT supported. The # and everything after it is part of the pattern.
Inline comments do not work in .gitignore. If a line contains text after the pattern, Git treats the entire line as the pattern, including the # character and everything that follows it.
Scope and Precedence¶
Git supports four places where ignore rules can live, each with a different scope.
Repository .gitignore¶
The most common case is a .gitignore file at the root of the repository. Patterns in this file apply to the entire repository relative to its location. This file is committed and shared with all contributors.
Subdirectory .gitignore¶
You can place a .gitignore file inside any subdirectory. Patterns in that file apply only to files within that subdirectory and its children. This is useful when a subdirectory has its own ignore requirements that do not belong in the root file.
Global .gitignore¶
A global ignore file applies to every repository on your machine, regardless of project. This is the right place for patterns that are specific to your editor, operating system, or development environment and that you would not want to impose on other contributors.
Configure the path to your global ignore file:
Common candidates for the global file:
# macOS
.DS_Store
.AppleDouble
.LSOverride
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini
# VS Code
.vscode/
*.code-workspace
# JetBrains IDEs
.idea/
*.iml
# Vim
*.swp
*.swo
*~
.git/info/exclude¶
Each repository has a file at .git/info/exclude that functions like a .gitignore but is never committed and never shared. It is the right place for machine-specific ignores that apply to a single repository but do not belong in the committed .gitignore. The format is identical to .gitignore.
Precedence¶
When multiple rules could apply to the same file, Git evaluates them in the following order from lowest to highest precedence, with later sources overriding earlier ones:
- The global file specified by
core.excludesFile .git/info/exclude.gitignorefiles, from the repository root down to the directory containing the file; a.gitignorecloser to the file takes precedence over one further up the tree
Within a single file, rules are evaluated top to bottom. The last matching rule wins.
Common Patterns by Ecosystem¶
Most language ecosystems and build tools have standard artifacts that should not be committed. A well-maintained .gitignore includes the right set from the start.
Go¶
# Compiled binaries
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test output
*.test
# Build output
/dist/
# Go workspace
go.work.sum
Python¶
# Byte-compiled files
__pycache__/
*.py[cod]
*$py.class
# Virtual environments
venv/
.venv/
env/
# Distribution and packaging
dist/
build/
*.egg-info/
# Testing
.pytest_cache/
.coverage
htmlcov/
Node.js¶
# Dependencies
node_modules/
# Build output
dist/
build/
# Environment files
.env
.env.local
.env.*.local
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
Terraform¶
# Local .terraform directories
.terraform/
# Terraform variable files (may contain secrets)
*.tfvars
*.tfvars.json
# State files (never commit these; use remote state)
*.tfstate
*.tfstate.*
# Crash logs
crash.log
crash.*.log
# Generated override files
override.tf
override.tf.json
*_override.tf
*_override.tf.json
Starting Point: github/gitignore
GitHub maintains a curated collection of .gitignore templates for dozens of languages and frameworks at github.com/github/gitignore. When starting a new project, grab the relevant template and adapt it rather than building from scratch. Many hosted platforms, including GitHub and GitLab, can pre-populate a .gitignore from these templates during repository creation.
Debugging Ignore Rules¶
When a file does not behave as expected, git check-ignore identifies which rule is responsible.
Check a Specific File¶
The output shows the file that contains the matching rule, the line number, the pattern, and the path that was matched:
If the file is not ignored, there is no output and the exit code is non-zero.
Check a File That Should Be Ignored but Is Not¶
If you added a file to .gitignore and it still appears in git status, the most common cause is that the file is already tracked. Remove it from the index first:
Then confirm the rule is working:
List All Ignored Files¶
To see every file Git is currently ignoring in the repository:
This is useful for auditing a .gitignore to confirm it is catching what you expect and not accidentally excluding files that should be tracked.
Dry-Run Before Cleaning
git clean removes untracked files from the working tree. Before running it, always use the -n flag for a dry run to confirm what will be deleted:
The -d flag includes directories, and -X removes only files ignored by Git. Review the output carefully before running without -n, as this operation cannot be undone.
Combining .gitignore with .gitkeep¶
A common pattern is to ignore the contents of a directory while preserving the directory itself in version control. Git does not track empty directories, so without a placeholder file the directory will not survive a fresh clone. The .gitignore negation mechanism handles this cleanly when combined with the .gitkeep convention.
# Ignore everything inside logs/
logs/*
# Keep the placeholder file so the directory is tracked
!logs/.gitkeep
Then create the placeholder and commit both files together:
touch logs/.gitkeep
git add logs/.gitkeep .gitignore
git commit -m "chore: track logs directory, ignore its contents"
Anyone who clones the repository gets a logs/ directory ready to use, and nothing written into it will appear in git status or be included in a commit. For more on the .gitkeep convention and why Git ignores empty directories in the first place, see Tracking Empty Directories in Git with .gitkeep.
References¶
- Git Documentation: gitignore: The authoritative reference for pattern syntax and matching rules.
- github/gitignore: GitHub's curated collection of
.gitignoretemplates organized by language and framework. - gitignore.io: A web tool for generating
.gitignorefiles from a list of languages and frameworks. - Tracking Empty Directories in Git with
.gitkeep: The companion post on preserving empty directories in Git.