Skip to content

.gitignore Patterns

A backend project often creates paths that are necessary on a developer’s machine but do not belong in normal version history: dependency directories, runtime logs, caches, generated output, and editor artifacts. Ignore rules let Git omit these paths while discovering untracked files.

Commands such as git status and broad forms of git add walk the Working Tree and look for paths that are not represented in the Index. Without ignore rules, generated paths create noisy status output and are easier to select by mistake.

Working Tree scan
│
├── path is already represented in the Index → tracked-path handling
│
└── path is untracked
│
├── no matching ignore rule → report as untracked
└── final matching rule ignores it → omit normally

An ignore rule does not delete, move, encrypt, or prevent the application from using a file. It only changes how Git’s untracked-path discovery treats it. Already tracked paths form a separate boundary covered in the next lesson.

The project usually stores shared rules in a file named .gitignore. That file is an ordinary project file and should normally be committed so every clone applies the same policy.

2. Exact files, directories, and extensions

Section titled “2. Exact files, directories, and extensions”

An exact path can identify one file relative to the .gitignore containing the rule:

storage/logs/laravel.log

A trailing slash means a directory and everything below it:

storage/logs/

This also ignores deeper paths such as storage/logs/orders/failed.log. Git does not store empty directories as independent objects; it stores files and their paths. A .gitkeep file is only a conventional ordinary file, not special Git syntax.

A pattern without a slash can match a name at any depth:

*.log

This matches both app.log and storage/logs/app.log, but not app.txt.

debug*.log

Matches debug.log and debug-api.log. It does not match api-debug.log because the filename must still begin with debug.

When a pattern contains path separators, a single * does not cross /:

logs/*.log

This targets .log files directly under logs/, not files in an arbitrarily deep descendant directory.

file?.log

Matches file1.log and fileA.log, but not file.log or file12.log.

logs/**/*.log

Matches logs/app.log, logs/api/error.log, and logs/api/orders/payment.log. It still requires the final .log suffix.

4. Anchor a rule to the ignore file’s directory

Section titled “4. Anchor a rule to the ignore file’s directory”

A leading slash anchors the pattern to the directory that contains this .gitignore. For a root .gitignore:

/logs/

This matches root logs/app.log, but not packages/api/logs/app.log. The leading slash does not mean “any directory root”; it means “start here, relative to this ignore file.”

Git can also read .gitignore files in subdirectories. Their patterns are relative to those directories, and a lower-level rule can be more specific than a rule encountered above it.

Prefix a pattern with ! to re-include a path excluded by an earlier rule:

*.log
!important.log

app.log stays ignored, while any file whose basename is important.log is not ignored by these rules. “Not ignored” does not mean “tracked”: the file is only eligible to appear as untracked until explicitly added.

Within the same rule source, the last matching rule wins. Order therefore matters.

There is an important traversal limitation. This usually does not re-include the child:

logs/
!logs/important.log

The parent directory itself was excluded, so Git normally does not traverse it to discover the exception. Ignore the contents instead when a direct child must be re-included:

logs/*
!logs/important.log

Nested exceptions can require re-including each excluded parent directory before the final file rule.

6. Comments, blank lines, and literal special characters

Section titled “6. Comments, blank lines, and literal special characters”

Blank lines do nothing. A line beginning with # is a comment:

# Runtime output
storage/logs/

Escape a leading special character when it is part of a real filename:

\#report.txt
\!important.txt

Trailing spaces are normally ignored unless escaped. Keep patterns simple and document non-obvious rules so teammates do not remove them accidentally.

7. Shared, repository-local, and global rules

Section titled “7. Shared, repository-local, and global rules”

Choose the rule source according to who needs the policy:

Source Scope Typical use
.gitignore Shared with the repository Generated paths common to the project
.git/info/exclude This local repository only Personal scratch paths for one checkout
core.excludesFile All repositories for one user OS or editor artifacts created everywhere

Configure a personal global excludes file, for example:

Terminal window
git config --global core.excludesFile ~/.gitignore_global

Global or local exclusion is convenience, not security. Other users do not receive those local rules, ignored files remain visible on the filesystem, and an intentional forced add can override ignoring.

At a high level, command-line exclusion patterns have the highest precedence, then per-directory .gitignore files from the path hierarchy, then .git/info/exclude, and finally core.excludesFile. Within one source level, the last matching rule decides.

Do not guess which of several rules matched. Ask Git:

Terminal window
git check-ignore -v storage/logs/app.log

Example output:

.gitignore:3:*.log storage/logs/app.log

The fields identify the rule source, line number, matched pattern, and tested path. A negation rule can also appear in verbose output; combine that evidence with git status to confirm whether an untracked path is available for normal addition. No output usually means no ignore pattern matched the tested untracked path.

Multiple paths can be tested together:

Terminal window
git check-ignore -v app.log important.log cache/data.tmp app.txt

The command is read-only.

Requirements:

  • Ignore all .log files.
  • Keep important.log eligible for tracking.
  • Ignore the complete cache/ directory.
  • Keep app.txt eligible for tracking.

Rules:

*.log
!important.log
cache/

Expected untracked status:

?? .gitignore
?? app.txt
?? important.log

app.log and cache/data.tmp are omitted. The .gitignore file itself is not special-cased; it appears as untracked until the project records it.

  • Saying “not ignored” means “tracked.” Tracking begins when content enters the Index.
  • Using /logs/ while intending to match nested directories named logs.
  • Expecting * to cross arbitrary directory separators.
  • Re-including a child after excluding its parent directory completely.
  • Forgetting that the last matching rule can reverse an earlier decision.
  • Assuming ignore rules hide files from people or protect sensitive data.
  • Editing patterns repeatedly without checking the winning rule with git check-ignore -v.
file.log → one relative path/name pattern
directory/ → directory and its descendants
*.log → matching basename at any depth
* → zero or more non-separator characters
? → exactly one non-separator character
** → match across directory depth
/logs/ → anchored to this .gitignore directory
!important.log → negate an earlier ignore match
git check-ignore -v → show the winning rule and source
Does ignoring a path remove it from the filesystem?

No. Ignore rules affect Git’s untracked-path discovery, not filesystem or application access.

Why does /logs/ not ignore packages/api/logs/ from a root ignore file?

The leading slash anchors the match to the root ignore file’s directory.

Why can logs/ followed by !logs/important.log fail?

The parent directory itself is excluded, so Git normally does not traverse it to discover and re-include the child.