open atlas
↑ Back to track
Git, from zero to senior GIT · 02 · 03

Ignoring files with .gitignore

A .gitignore file lists glob patterns for paths git should leave untracked. Learn anchoring, directory and negation rules, the ** wildcard, and the key gotcha: gitignore only affects UNTRACKED files — an already-tracked file needs git rm --cached to stop being committed.

GIT Junior ◷ 16 min
Level
FoundationsJuniorMiddleSenior

Not everything in your project folder belongs in version control. Compiled output, a 200 MB node_modules, editor settings, and — most dangerously — a .env file full of secrets all sit right next to your source. Commit them and you bloat the repo, leak credentials, and trigger merge conflicts on machine-specific junk. The filter that keeps them out is .gitignore.

By the end of this lesson you will be able to write .gitignore patterns precisely — anchored, recursive, negated — and fix the number-one gitignore failure: a file that keeps getting committed even though you “ignored” it.

Goal

After this lesson you can write .gitignore glob patterns for files, directories, anchored paths, and exceptions, explain why gitignore only affects untracked files, and use git rm --cached to stop tracking a file that is already in history.

1

A .gitignore file lists patterns for paths git should leave untracked. You place it at the repo root (or in any subdirectory, scoping it to that folder), and git skips matching files in git status and git add .:

# comments start with a hash
*.log            # ignore every file ending in .log
node_modules/    # ignore the whole node_modules directory
.env             # ignore the secrets file
build            # ignore anything named build (file or dir, anywhere)

Each line is a glob pattern. A blank line matches nothing (used for spacing); a line starting with # is a comment. The .gitignore file itself is normally committed — it is shared so the whole team ignores the same things.

2

Glob patterns: * matches within a path segment, ** matches across directories. The wildcards are the core vocabulary:

*.log            # any file ending .log, in any directory
temp?.txt        # temp1.txt, tempA.txt (? = one character)
logs/**/*.log    # .log files at any depth under logs/
**/cache/        # a cache directory anywhere in the tree

* stops at a slash — it matches within one directory level. ** is the recursive wildcard that spans directory boundaries. Most real .gitignore lines are just a *.ext extension rule or a dir/ directory rule; the heavier patterns come up rarely.

3

A leading slash anchors a pattern to the .gitignore’s own directory; without one, it matches anywhere. This distinction trips up many people:

/build           # ONLY the build dir at the repo root
build            # ANY file/dir named build, at any depth

build (no slash) ignores build, src/build, lib/x/build — every build in the tree. /build ignores only the top-level one, leaving a src/build source file tracked. A trailing slash like build/ further restricts the match to directories, never a file named build.

4

A leading ! negates a pattern, re-including a file an earlier rule excluded. Use it to ignore a whole class but keep an exception:

*.log            # ignore all logs
!important.log   # ...except this one — keep tracking it

Order and parent directories matter: you cannot re-include a file if its parent directory is excluded, because git never descends into an ignored directory to even see the exception. To make !secrets/keep.txt work, ignore the contents (secrets/*) rather than the directory itself (secrets/), so git still looks inside.

Common mistake

The critical gotcha: .gitignore only affects UNTRACKED files. If a file is already tracked — committed even once — adding it to .gitignore does nothing; git keeps recording its changes. This is the classic “I added .env to gitignore but it still shows up.” The fix is to stop tracking it while keeping it on disk:

git rm --cached .env      # untrack it; the file stays on your disk
echo ".env" >> .gitignore # ensure it is ignored going forward
git commit -m "Stop tracking .env; ignore it"

git rm --cached removes the file from the index (so the next commit drops it) but leaves your working-tree copy alone. Note: the file’s contents still live in past commits — if it held real secrets, rotate them, because history can be retrieved.

Worked example

A secret that won’t stay ignored.

Early in the project you committed .env by accident. Weeks later you add it to .gitignore, but git status still shows .env as modified every time you edit it. The reason: it is already tracked, so the ignore rule is inert.

git rm --cached .env
# rm '.env'   — removed from the index, still on disk
cat .gitignore
# .env        — already present, good
git commit -m "Stop tracking .env and ignore it"

Now .env is untracked and the gitignore rule finally applies — future edits no longer appear in git status. But the old commits still contain whatever was in .env when you first committed it, so you rotate those credentials. For everything else machine-specific, you reach for a ready-made template: GitHub’s github/gitignore repo and gitignore.io generate language- and tool-specific files (Node, Python, JetBrains, macOS) so you do not hand-write every rule.

Why this works

Why does git ignore the ignore file for tracked paths? Because .gitignore is about deciding what enters version control, not about deleting recorded history. Git treats a tracked file as a deliberate decision you already made; silently dropping it from future commits because a pattern appeared would be surprising and dangerous. Making you run git rm --cached is git forcing the decision to be explicit — the same philosophy as the staging area: nothing leaves or enters history by accident.

Check yourself
Quiz

You committed config.json months ago. Today you add config.json to .gitignore, but git still tracks your edits to it. Why, and what fixes it?

Recap

A .gitignore file lists glob patterns for paths git should leave untracked: *.log (extension), node_modules/ (directory), # (comment). A leading slash anchors a pattern to the file’s directory (/build = root only) versus matching anywhere (build); ** is the recursive wildcard; a leading ! negates, re-including an exception (but not if its parent directory is excluded). The critical gotcha: gitignore only affects untracked files — an already-tracked file ignores the rule until you git rm --cached <file> to drop it from the index while keeping it on disk (and rotate any secrets, since history retains the old contents). Reach for github/gitignore templates or gitignore.io instead of hand-writing rules. Next you will learn what separates a good commit from a noisy one.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.