.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.
1. The problem .gitignore solves
Section titled “1. The problem .gitignore solves”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 normallyAn 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.logA 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:
*.logThis matches both app.log and storage/logs/app.log, but not app.txt.
3. Wildcards
Section titled “3. Wildcards”* — zero or more characters
Section titled “* — zero or more characters”debug*.logMatches 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/*.logThis targets .log files directly under logs/, not files in an arbitrarily
deep descendant directory.
? — exactly one character
Section titled “? — exactly one character”file?.logMatches file1.log and fileA.log, but not file.log or file12.log.
** — directory-depth matching
Section titled “** — directory-depth matching”logs/**/*.logMatches 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.
5. Negation and rule order
Section titled “5. Negation and rule order”Prefix a pattern with ! to re-include a path excluded by an earlier rule:
*.log!important.logapp.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.logThe 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.logNested 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 outputstorage/logs/Escape a leading special character when it is part of a real filename:
\#report.txt\!important.txtTrailing 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:
git config --global core.excludesFile ~/.gitignore_globalGlobal 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.
8. Verify the winning rule
Section titled “8. Verify the winning rule”Do not guess which of several rules matched. Ask Git:
git check-ignore -v storage/logs/app.logExample output:
.gitignore:3:*.log storage/logs/app.logThe 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:
git check-ignore -v app.log important.log cache/data.tmp app.txtThe command is read-only.
9. Worked example
Section titled “9. Worked example”Requirements:
- Ignore all
.logfiles. - Keep
important.logeligible for tracking. - Ignore the complete
cache/directory. - Keep
app.txteligible for tracking.
Rules:
*.log!important.logcache/Expected untracked status:
?? .gitignore?? app.txt?? important.logapp.log and cache/data.tmp are omitted. The .gitignore file itself is
not special-cased; it appears as untracked until the project records it.
10. Common mistakes
Section titled “10. Common mistakes”- Saying “not ignored” means “tracked.” Tracking begins when content enters the Index.
- Using
/logs/while intending to match nested directories namedlogs. - 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.
11. Summary
Section titled “11. Summary”file.log → one relative path/name patterndirectory/ → 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 matchgit check-ignore -v → show the winning rule and source12. Knowledge check
Section titled “12. Knowledge check”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.