Ignoring Files with .gitignore
Reviewed & published by Brayan K
Not everything belongs in version control — build outputs, dependencies, secrets, and editor junk should stay out. By the end of this lesson you'll write .gitignore patterns confidently, use negation, untrack files you already committed, and set up a global ignore.
Part of the free Git course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Write .gitignore patterns with globs (* and **)
- Ignore directories with a trailing slash
- Re-include files with negation (!)
- Understand why .gitignore can't untrack a tracked file
- Untrack a committed file with git rm --cached
- Set up a global gitignore for personal clutter
1️⃣ Writing .gitignore
A .gitignore file lives at your repo root. Each line is a pattern: a bare name ignores one file, *.log ignores every file with that extension, and a trailing slash like build/ ignores a whole directory. Lines starting with # are comments. Commit the .gitignore so your whole team shares the rules.
# .gitignore is a plain text file at your repo root. Each line is a
# PATTERN of files Git should ignore (never track, never nag you about).
# A comment — Git skips lines starting with #
# Ignore a single specific file
secrets.env
# Ignore every file with a given extension (the * wildcard)
*.log
# Ignore a whole directory (the trailing slash means "directory")
node_modules/
build/
# Ignore everything in a folder but a specific file inside it
logs/*
!logs/.gitkeep2️⃣ Patterns, Globs & Negation
Patterns give you fine control. A leading slash anchors to the root, ** matches across any number of directories, and a leading ! negates an earlier rule to re-include a file. Test any path with git check-ignore -v <path> — it prints the matching rule, or nothing (exit code 1) if the path isn't ignored.
# Pattern cheat-sheet — test any path with: git check-ignore -v <path>
*.tmp # any file ending in .tmp, anywhere
/config.local # a leading slash anchors to the repo ROOT only
build/ # the build directory (trailing slash = directory)
**/cache # a "cache" folder at ANY depth (** = any directories)
logs/**/debug.log # debug.log under logs/, however deeply nested
# NEGATION — re-include something an earlier rule ignored
*.log # ignore all logs...
!important.log # ...except this one (the ! un-ignores it)3️⃣ Untracking an Already-Committed File
The single biggest gotcha: .gitignore only affects untracked files. If a file is already tracked, adding it to .gitignore does nothing — Git keeps committing it. To stop, untrack it with git rm --cached, which removes it from Git's index but keeps it on disk. Then add it to .gitignore and commit.
# ⚠️ The #1 gotcha: .gitignore only affects UNTRACKED files.
# If a file is ALREADY tracked, adding it to .gitignore does nothing —
# Git keeps committing it. You must untrack it first.
# Stop tracking the file but KEEP it on disk
git rm --cached secrets.env
# Now add it to .gitignore so it stays ignored, then commit the change
echo "secrets.env" >> .gitignore
git commit -m "Stop tracking secrets.env"
# To untrack a whole folder, add -r (recursive)
git rm -r --cached build/4️⃣ A Global gitignore
Some clutter — editor swap files, .DS_Store, IDE folders — appears in every project but shouldn't pollute each project's shared .gitignore. Set up a global ignore once with git config --global core.excludesfile, and it applies to all your repos.
Your turn. Ignore all logs but keep one, then untrack a file you committed by mistake. Fill in the three blanks.
# 🎯 YOUR TURN — replace each ___ then run it in your terminal.
# Goal: ignore build junk but keep one important log.
# 1) Ignore every .log file (one pattern)
echo "___" >> .gitignore # 👉 the glob that matches all .log files
# 2) But re-include important.log (negation)
echo "___important.log" >> .gitignore # 👉 the character that negates
# 3) You already committed debug.log by mistake — untrack it (keep on disk)
git ___ --cached debug.log # 👉 the command that untracks a tracked file
# ✅ Expected: "git check-ignore debug.log" prints it (ignored), while
# "git check-ignore important.log" prints nothing (the ! kept it tracked).Common Errors (and the fix)
- ".gitignore isn't ignoring my file": it's probably already tracked. Untrack with git rm --cached <file>, then commit.
- Negation not working: a ! rule must come after the rule it negates, and you can't re-include a file if its parent directory is ignored.
- Committed a secret already: untracking with git rm --cached stops future commits, but the secret stays in history — rotate it and consider history-rewriting tools.
- Empty folder won't commit: Git doesn't track empty directories. Add a placeholder file like .gitkeep and negate it.
Pro Tips
- 💡 Start from a template: github.com/github/gitignore has battle-tested files for every language and tool.
- 💡 Debug with check-ignore: git check-ignore -v <path> tells you exactly which rule matched.
- 💡 Ignore early: add .gitignore at project start, before junk gets committed.
- 💡 Global for you, project for the team: keep personal editor junk in the global ignore.
📋 Quick Reference
| Pattern / Command | Meaning |
|---|---|
| *.log | Ignore all files ending in .log |
| build/ | Ignore the build directory |
| /config.local | Ignore only at the repo root |
| **/cache | Ignore "cache" at any depth |
| !important.log | Negate — re-include this file |
| git rm --cached file | Untrack a file, keep it on disk |
| git check-ignore -v path | Show which rule ignores a path |
Mini-Challenge: Clean Up a Messy Repo
No commands given this time — just the plan. Write a .gitignore that keeps only your real source files tracked.
# 🎯 MINI-CHALLENGE: clean up a messy repo.
# No commands given — just the plan. Run each step in your terminal.
#
# 1. Create files: app.js, debug.log, node_modules/lib.js, .env
# 2. Write a .gitignore that ignores *.log, node_modules/, and .env
# 3. Run "git status" — only app.js and .gitignore should show as untracked
# 4. Pretend .env was already committed: untrack it with git rm --cached
# 5. Prove each rule with "git check-ignore -v <path>"
#
# ✅ Expected: status shows only app.js + .gitignore; check-ignore confirms
# debug.log, node_modules/lib.js, and .env are all ignored.
# your commands here🎉 Lesson Complete!
- ✅ .gitignore lists patterns of files Git should not track
- ✅ * matches within a segment, ** across folders, trailing / means directory
- ✅ ! negates an earlier rule to re-include a file
- ✅ .gitignore can't untrack a tracked file — use git rm --cached
- ✅ A global ignore (core.excludesfile) holds personal clutter for all repos
Practice quiz
What is the purpose of a .gitignore file?
- To delete files
- To list your commits
- To tell Git which untracked files to ignore
- To store passwords safely
Answer: To tell Git which untracked files to ignore. .gitignore lists patterns of files Git should not track or show as untracked.
What does the pattern `*.log` match?
- Every file ending in .log
- Only a file literally named *.log
- Files in a log/ folder
- Nothing
Answer: Every file ending in .log. The * wildcard matches any characters, so *.log matches all files with the .log extension.
What does a trailing slash mean, as in `build/`?
- Ignore a file named build
- Ignore files starting with build
- It's invalid syntax
- Ignore only the build directory (and its contents)
Answer: Ignore only the build directory (and its contents). A trailing slash restricts the match to directories.
What does a leading `!` do, as in `!important.log`?
- Forces deletion
- Negates a previous ignore — re-includes the file
- Comments out the line
- Matches hidden files
Answer: Negates a previous ignore — re-includes the file. ! negates an earlier pattern, so a file otherwise ignored becomes tracked again.
A file is ALREADY tracked. Adding it to .gitignore...
- Has no effect until you run git rm --cached on it
- Immediately untracks it
- Deletes it
- Causes an error
Answer: Has no effect until you run git rm --cached on it. .gitignore only affects untracked files; you must git rm --cached an already-tracked file to stop tracking it.
What does `git rm --cached secrets.env` do?
- Deletes the file from disk
- Adds it to the next commit
- Stops Git tracking the file but keeps it on disk
- Ignores all .env files
Answer: Stops Git tracking the file but keeps it on disk. --cached removes the file from the index (untracks it) while leaving the working-tree copy intact.
Lines starting with `#` in .gitignore are...
- Negations
- Comments
- Directory patterns
- Errors
Answer: Comments. A # at the start of a line marks a comment in .gitignore.
Which sets up a GLOBAL gitignore for all your repos?
- git ignore --global
- echo to .gitignore-all
- git global-ignore
- git config --global core.excludesfile ~/.gitignore_global
Answer: git config --global core.excludesfile ~/.gitignore_global. Setting core.excludesfile points Git at a personal ignore file applied across all repositories.
What does `**` mean in a pattern like `logs/**/debug.log`?
- Exactly one directory
- Any number of directories (including none)
- Only hidden directories
- A literal asterisk
Answer: Any number of directories (including none). ** matches across multiple directory levels.
Where should project-specific ignore rules normally live?
- In your home folder only
- In the commit message
- In a .gitignore file committed at the repo root
- In a branch name
Answer: In a .gitignore file committed at the repo root. A committed .gitignore at the repo root shares the rules with the whole team.
Continue this course
- Previous: Stashing Work in Progress
- Next: Exploring History with git log — Read project history with log, --oneline --graph, filtering, and git show
- Quick reference: Git cheat sheet › .gitignore & Misc
- From the blog: Essential Git Commands for Developers
Frequently asked questions
Why isn't my .gitignore working?
The most common reason is that the file is already tracked. .gitignore only affects untracked files — once Git is tracking something, adding it to .gitignore does nothing. Run git rm --cached <file> to untrack it (the file stays on disk), then commit. Other causes: a typo in the pattern, or the rule being overridden by a later negation. Use git check-ignore -v <path> to see exactly which rule matches.
What's the difference between *, **, and a trailing slash?
A single * matches any characters within one path segment, so *.log matches files ending in .log. ** matches across directory levels, so logs/**/debug.log matches debug.log however deeply it's nested under logs/. A trailing slash like build/ restricts the match to a directory rather than a file of the same name.
How do I ignore everything in a folder except one file?
First ignore the folder's contents, then negate the file you want to keep: logs/* on one line, !logs/.gitkeep on the next. Order matters — the negation must come after the broader ignore rule. A common use is committing an empty placeholder file (often named .gitkeep) so Git keeps an otherwise-ignored directory in the repo.
What is git rm --cached and when do I use it?
git rm --cached <file> removes a file from Git's index (stops tracking it) while leaving the actual file on your disk. You use it when you accidentally committed something that should have been ignored — like a secrets file or a build artifact. After untracking it, add the file to .gitignore and commit, so it stays out of the repo going forward.
Should I use a project .gitignore or a global one?
Use both, for different things. A project .gitignore lives at the repo root, is committed, and is shared with the whole team — it should list project-specific outputs like build/ or dist/. A global gitignore (set with git config --global core.excludesfile) applies to all your repos and should hold your personal clutter like editor swap files and OS junk (.DS_Store), which don't belong in any shared project file.