git.guide

Why your ignore file is not working

Nine times out of ten the answer is the same. An ignore file only affects files git is not already tracking, so adding a line for something you have already committed changes nothing at all.

The answer

Ignoring is about untracked files, and only about untracked files.

The rule git follows is narrow and it is not obvious from the name. An ignore file tells git which files to leave out when it is deciding what is new. Once a file has been committed, git is tracking it, and tracking beats ignoring every time. Your changes keep showing up in git status and you keep committing them.

To stop tracking a file while leaving it on your disk:

git rm --cached .env
git commit -m "Stop tracking the environment file"

For a whole directory, add -r:

git rm -r --cached node_modules
git commit -m "Stop tracking installed dependencies"

The --cached is what keeps the file. Leave it out and git deletes the real thing off your disk as well, which for an environment file on a working server is a genuinely bad afternoon.

Check what you are about to do first. This lists everything git currently tracks that your ignore rules say it should not:

git ls-files --ignored --exclude-standard -c

And this is the important half

Untracking it does not remove it from the history.

This is where people stop too early, and it matters most for exactly the file they are usually trying to ignore.

Untracking a file removes it going forward. Every commit that already contains it still contains it. If the file was an environment file with a live database password in it, that password is still in the repository, still readable by anybody who can read the repository, and the commit that removed it does not change that.

So the sequence is untrack it, ignore it, and then treat every secret it held as published and change them.

What to do when a password is already in the history, which is a different job from this one and is the one that actually protects you.

The pattern rules

The five that account for most confusion

A trailing slash means directory only

build/     # the directory called build, at any depth
build      # anything called build, file or directory

A leading slash anchors to the repository root

/config.php     # only the one at the top
config.php      # any file of that name, anywhere

A slash anywhere in the middle also anchors it

This one is genuinely surprising. A pattern with a slash in the middle is matched from the root rather than at any depth.

doc/notes.txt      # only /doc/notes.txt
**/doc/notes.txt   # any doc/notes.txt at any depth

An exclamation mark re-includes something

*.log
!important.log

With one large exception. You cannot re-include a file if one of its parent directories is excluded, because git never looks inside an excluded directory in the first place. This does not work:

uploads/
!uploads/keep-me.txt     # never seen. uploads/ was already ruled out

Exclude the contents rather than the directory, then make your exception:

uploads/*
!uploads/keep-me.txt

Later rules beat earlier ones

The last pattern that matches a given path decides. So order matters, and a broad rule near the bottom of the file will quietly override the careful exception you wrote at the top.

Which file, and where

There are four places rules can live, and they answer different questions.

WhereFor
.gitignore in the repository Anything true for everybody working on this project. Build output, dependencies, environment files. This one is committed and shared, and it is where almost everything belongs
A .gitignore in a subdirectory Rules that only make sense for one part of the project. Paths in it are relative to that directory
A global ignore file Your own tools, on your own machine. Editor directories, operating system clutter. Nobody else should have to carry these
.git/info/exclude Rules for this one clone that are nobody else's business. Not committed, not shared

Set the global one up once and stop putting editor directories in other people's projects:

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

To find out which rule is responsible for a file being ignored, ask git rather than reading the file and guessing. It names the file and the line:

git check-ignore -v path/to/file

Day one

The four lines that save the most trouble

Written before the first commit rather than after the first mistake, because after the first commit the file is tracked and the section at the top of this page applies.

# Secrets. Never committed, not once, not even briefly
.env
.env.*
!.env.example

# Installed things. Reproducible from the lock file
node_modules/
vendor/

# Build output
dist/
build/

# Editor and operating system clutter
.DS_Store
Thumbs.db
.idea/
.vscode/

Commit an example file alongside it, listing every setting by name with the values left blank. That documents what the project needs without carrying anything sensitive, and it is why the !.env.example line is there.

Ignoring is not a security control. It stops an accident, and it stops it only if it is in place first. The control is that the secret is not in the repository at all, which is what configuration from the environment is for.

One more thing worth ignoring

If your deploy copies the project folder to a server, the ignore file has nothing to do with what gets copied. Ignoring is about what git tracks, not about what your file transfer sends, and the two are constantly confused.

An rsync with no exclusions will happily upload the repository itself along with everything you thought you had ignored, which is the leak this site checks for.

Short answers

Questions people actually ask

I added it to my ignore file and it still shows up. Why?

Because the file is already tracked, and tracking beats ignoring. An ignore file only decides what git treats as new. Once a file has been committed, the ignore rule has no effect on it at all. git rm --cached untracks it while leaving it on your disk.

How do I stop tracking a file without deleting it?

git rm --cached path, then commit. The --cached is the part that keeps the file. Leave it out and git deletes the real thing, which for a live environment file on a working server is a genuinely bad afternoon.

Does untracking it remove it from the history?

No. Every commit that already contains the file still contains it. If it held a credential, that credential is still published and changing it is the fix. Untracking only stops the file going forward.

Why will my exception pattern not work?

Almost always because a parent directory is excluded, and git never looks inside an excluded directory to find your exception. Exclude the contents rather than the directory, so uploads/* followed by !uploads/keep.txt rather than uploads/.

Which rule is ignoring my file?

git check-ignore -v path. It names the file and the line responsible, which saves reading four ignore files and guessing. Remember that the last matching pattern wins, so a broad rule at the bottom quietly overrides a careful exception at the top.

Where should my own editor and operating system clutter go?

In a global ignore file on your machine, not in the project. Set it once with git config --global core.excludesFile and stop putting your editor directory in other people's repositories.

Every question on this site, in one list

A repository that has got too big

What one committed database dump costs, and how long it keeps costing it.

Branches, merging and rebasing

What a branch actually is, and the one rule you must not break.

Pushing, pulling and the errors you will hit

Rejected pushes, force with lease, and how to leave a detached HEAD.

All nine guides