How .gitignore Works -- Patterns, Examples, and Common Mistakes

Master .gitignore syntax with pattern examples for Node.js, Python, Java, and other common project types.

Understanding .gitignore Basics

The .gitignore file is one of the most important files in any version control project. It tells Git which files and directories to exclude from version tracking. Without a proper .gitignore, you risk committing sensitive data like API keys, node_modules folders, compiled binaries, or local environment configuration files that shouldn't be shared across your team.

A .gitignore file is just a plain text file placed in your project root (or in any subdirectory) containing patterns that match file paths Git should ignore. When Git encounters a file path that matches a pattern in .gitignore, it won't stage, commit, or track that file.

Basic Pattern Syntax

Wildcard Patterns

The asterisk (*) is the most common pattern character. It matches any number of characters except forward slashes.

*.log
*.tmp
__pycache__/

This ignores all .log files, all .tmp files, and any __pycache__ directory at any level.

Question Mark Pattern

The question mark (?) matches exactly one character.

file?.txt

This would match file1.txt or fileA.txt, but not file.txt or file12.txt.

Character Classes

Square brackets specify character ranges or sets to match.

[abc].txt        # matches a.txt, b.txt, or c.txt
[0-9].log        # matches 0.log through 9.log
[!abc].txt       # matches any character except a, b, or c

Double Asterisk Pattern

The double asterisk (**) matches across directory levels.

**/node_modules  # matches node_modules at any depth
src/**/build     # matches build directory anywhere under src/
**/*.pyc         # matches .pyc files anywhere in the project

Directory-Only Patterns

To match only directories and not files with the same name, add a trailing slash.

build/           # ignores the build directory and everything in it
dist/            # ignores the dist directory and everything in it
.env/            # ignores an .env directory (unusual but valid)

Without the trailing slash, Git would ignore both a directory named build and any file named build.

Negation Patterns

The exclamation mark (!) negates a pattern, creating exceptions to ignoring rules.

*.log            # ignore all .log files
!important.log   # except important.log

In this example, all .log files are ignored except important.log, which will be tracked.

Important caveat: You cannot unignore a file inside an ignored directory. If you ignore dist/, you cannot selectively unignore dist/specific-file.txt. You must first unignore the directory:

dist/            # ignore everything
!dist/           # but not this directory
!dist/build.js   # now we can selectively track files

Comments and Blank Lines

Lines starting with # are treated as comments and ignored. Blank lines have no effect.

# Environment variables
.env
.env.local
.env.*.local

# Dependencies (this is a comment)
node_modules/
vendor/

# Build output
dist/
build/

Common Project-Specific Templates

Node.js Projects

# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
yarn.lock

# Environment
.env
.env.local
.env.*.local

# IDE
.vscode/
.idea/
*.swp
*.swo

# Build output
dist/
build/

Python Projects

# Virtual environments
venv/
env/
ENV/
.venv

# Python bytecode
__pycache__/
*.py[cod]
*$py.class
*.so

# Distribution
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# IDE
.vscode/
.idea/
*.swp
*.swo

# Testing
.pytest_cache/
.coverage
htmlcov/

# Environment
.env
.env.local

Java Projects

# Compiled files
*.class
*.jar
*.war
*.ear

# Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
.flattened-pom.xml

# Gradle
.gradle
build/
gradle-app.setting
!gradle-wrapper.jar

# IDE
.vscode/
.idea/
*.swp
*.swo
*.iml
.classpath
.project
.settings/

# Environment
.env
.env.local

Nested .gitignore Files

You can place .gitignore files in subdirectories to add additional ignore patterns specific to that directory.

project/
  .gitignore          # root ignores
  src/
    .gitignore        # additional patterns for src/
  tests/
    .gitignore        # additional patterns for tests/

Patterns in nested .gitignore files apply to that directory and its subdirectories. This is useful for excluding test artifacts only from the tests directory or generated files from specific folders without cluttering the root .gitignore.

Debugging: Why .gitignore Isn't Working

The most common reason .gitignore appears broken is that files were already committed to Git before being added to .gitignore. Once a file is tracked by Git, adding it to .gitignore won't prevent future changes from being staged.

To fix this, remove the file from Git's tracking without deleting it locally:

git rm --cached filename.txt

For an entire directory:

git rm --cached -r directory/

Then commit this removal:

git commit -m "Stop tracking filename.txt"

Using git check-ignore

To debug which pattern is causing a file to be ignored, use git check-ignore:

git check-ignore -v filename.txt

The -v flag shows which pattern matched. Example output:

.gitignore:5:*.log    debug.log

This tells you that debug.log is ignored by the pattern *.log on line 5 of .gitignore.

Global .gitignore

In addition to project-level .gitignore, Git allows you to configure a global ignore file that applies to all repositories on your machine. This is useful for personal files like IDE settings or OS-specific files.

Create a global gitignore file (typically ~/.gitignore_global):

git config --global core.excludesfile ~/.gitignore_global

Then populate it with patterns for your personal development environment:

# IDE
.vscode/
.idea/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Local
.local/
.private/

Common Mistakes

Mistake 1: Forgetting the trailing slash for directories

# Bad - might not match the directory
node_modules

# Good - explicitly match the directory
node_modules/

Mistake 2: Using patterns before unignoring parent directory

# This won't work
dist/
!dist/important.js

# Do this instead
!dist/
!dist/important.js

Mistake 3: Committing files before adding them to .gitignore

Files already tracked by Git will continue to be tracked even if added to .gitignore. Use git rm --cached to remove them from tracking first.

Mistake 4: Using absolute paths

# Bad - too specific to your machine
/home/user/project/secrets.txt

# Good - relative to repo root
secrets.txt

Mistake 5: Ignoring the entire src directory to ignore just one subdirectory

# Bad
src/

# Good
src/temp/
src/build/

Best Practices

  1. Create a .gitignore early in project setup, before committing sensitive files
  2. Use existing templates - services like GitHub provide language-specific .gitignore templates
  3. Review before committing - always check git status to ensure you're not accidentally committing ignored files
  4. Document your patterns - add comments explaining why certain patterns exist
  5. Keep it maintainable - organize patterns by category (dependencies, build output, IDE, etc.)
  6. Test your patterns - use git check-ignore -v to verify patterns work as expected

Conclusion

Mastering .gitignore is essential for maintaining clean repositories and avoiding accidentally committing sensitive or unnecessary files. Start with language-specific templates, understand the pattern syntax, and always verify your configuration works before sharing your code with teammates. A well-configured .gitignore is invisible to other developers but prevents countless problems.

Related Tools