Skip to content
shellhacks.dev
Git

How to Use gitignore and Global Ignore Files the Right Way

Write gitignore patterns that match, use global and per-repository ignore files, find why a path is ignored with check-ignore, and know the limits.

6 min read
Summarize in AI
Illustration for "How to Use gitignore and Global Ignore Files the Right Way": a terminal session.

An ignore file keeps build output, dependency folders and editor droppings out of git status and out of accidental commits. Writing the patterns correctly, and putting them in the right file, is the part that goes wrong. The rules below are from Git 2.45, and the common follow-up problem, a file that is still tracked, is in stopping tracking a file.

Pattern rules that matter

Text
# comments start with a hash
*.log              # any .log file at any depth
/build/            # only the top-level build directory
node_modules/      # a directory named node_modules anywhere
docs/*.tmp         # .tmp files directly inside docs
**/cache/          # a cache directory at any depth
!important.log     # re-include a file

A pattern without a slash matches at any depth. A leading slash anchors it to the directory containing the .gitignore. A trailing slash matches only directories. * does not cross a slash, while ** does. The ! line must come after the rule it overrides, and it cannot re-include a file if a parent directory is itself ignored, since Git never looks inside it.

Three places for ignore rules

Shell
git config --global core.excludesFile ~/.config/git/ignore
printf '.idea/\n.vscode/\n.DS_Store\n' >> ~/.config/git/ignore
echo 'scratch/' >> .git/info/exclude

Rules about the project belong in its committed .gitignore, such as build directories and generated files. Rules about your own tools, such as editor settings and OS junk, belong in the global ignore file, so every repository benefits without adding clutter to each project. Git reads $XDG_CONFIG_HOME/git/ignore by default, without any setting. .git/info/exclude works like a .gitignore that is local and never committed, which suits private scratch files.

Find out why something is ignored

Shell
git check-ignore -v build/app.js
git status --ignored
git ls-files --others --ignored --exclude-standard
Text
.gitignore:3:/build/	build/app.js

The output names the file, line number and pattern that matched. When a file is missing from git status and you did not expect it, check-ignore -v answers faster than reading every ignore file. Add -n to see paths that are not ignored as well.

Ignoring everything except

Text
/*
!/.gitignore
!/src/
!/README.md

This allow-list style ignores all top-level entries except the named ones. It works because the exceptions are at the top level; an exception deep inside needs each parent directory re-included in turn, which is why such files get long.

How Git applies the rules

When Git considers a path, it checks the ignore sources in a fixed order: patterns given on the command line, then the .gitignore files from the directory of the path up to the top of the repository, with deeper files taking priority, then .git/info/exclude, then the global file named by core.excludesFile. Within one file, the last matching pattern wins, which is why a negation must come after the rule it overrides. A negation cannot re-include a file if a parent directory is excluded, because Git does not descend into an ignored directory, so to keep one file inside an ignored folder you must ignore the folder's contents with a star and not the folder itself, then un-ignore the file, as the pattern list in the post shows. This single rule explains most questions about an exception that has no effect questions.

Matching uses the glob rules of fnmatch with extras: a star matches within one path component, a double star matches across directories, a question mark matches one character, and brackets match a set. Trailing spaces are ignored unless escaped with a backslash, and a leading hash starts a comment, so a file whose name begins with a hash needs a backslash. Windows path separators are not valid; always use forward slashes.

What belongs in the project file

The project's .gitignore should list what any contributor would generate: build output, dependency directories, compiled objects, test coverage reports, language caches, local environment files. It should not list one person's editor or operating system droppings; those go in the global file, which keeps the project file short and reduces arguments. Start from a maintained template for your language, such as those collected at github.com/github/gitignore, and trim what does not apply. Commit the file with the first commit, so build output never enters history, and review it when the toolchain changes.

Environment files and secrets

Local configuration files with passwords and keys are the most important entries. Ignore the real files by name, such as .env and *.pem, and commit examples with placeholders. An ignore rule is a guard against accidents and not a security control: git add -f overrides it, a rule added after a file was committed has no effect, and a renamed file escapes a name-based rule. Combine it with a pre-commit secret scanner, and with server-side push protection where available. If a secret is committed anyway, rotate it before anything else.

Debugging ignore behaviour

git check-ignore -v path prints the file, line and pattern that ignore a path, which resolves most mysteries, and git status --ignored lists everything currently ignored. Add -n to git clean to preview which untracked files it would delete, and add -x only when you also want ignored files removed, as in a fresh rebuild. For a path that is tracked but should be ignored, remember that the ignore file has no effect until the file is removed from the index with git rm --cached. When a pattern seems right but matches nothing, check for a trailing slash that restricts it to directories, a leading slash that anchors it to the wrong directory, and a nested .gitignore that overrides it.

Other tools that read ignore files

Many tools adopt the same format: Docker reads .dockerignore to decide what goes into the build context, npm uses .npmignore, and search tools like ripgrep respect .gitignore by default. The syntax is similar but not identical, and Docker's version anchors patterns to the context root and handles the double star differently, so copying a Git file over is a starting point and not a finish. A good .dockerignore also makes builds faster and images smaller, because the context sent to the daemon shrinks.

Where this does not apply

.gitignore is not a security measure: it does not stop git add -f, and it does not remove a file that was committed earlier, including a secret, covered in removing a secret from history. Empty directories cannot be tracked, so the usual trick is a .gitkeep file, which has no special meaning to Git. Ignore rules are not shared between tools that do not read them. See the gitignore manual for the full pattern format and the github/gitignore collection for templates per language.

When a team keeps adding patterns, group them with comments by purpose, such as build output, dependency directories and secrets, and review the file when the build system changes. A stale rule can hide a file that should be tracked, and git check-ignore -v is the quickest check. Related fixes are in stopping tracking a file, and large binaries are better handled by Git LFS than by ignore rules.

Taggedgit
Share

Keep reading

Get new posts by email

One email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.