How to make Git forget a file that is already tracked

You committed something you should not have — a .env, a build directory — then added it to .gitignore and it kept showing up. That is not a broken ignore rule. .gitignore is not consulted for files git already tracks.

Why the ignore rule does nothing

.gitignore : .env
tracked    : .env .gitignore app.py

The file is ignored and tracked at the same time, and tracking wins:

after editing it, status :  M .env

.gitignore decides which untracked files git mentions and picks up with git add .. Once a file is in the index, that list is not consulted for it any more. The rule is doing exactly what it says; it just does not apply to this file.

Untracking it

$ git rm --cached .env
$ git commit -m "stop tracking .env"
tracked : .gitignore app.py
on disk : .env .git .gitignore app.py
status  : D  .env

The D looks alarming. It means a deletion is staged — from git’s point of view the file is going away, which is the whole point. The file itself is still on disk, as the listing shows.

After the commit it is untracked, .gitignore applies to it for the first time, and git status is quiet:

after commit, status : ''

One flag away from deleting it

$ git rm .env          # removes it from the index AND from disk
after 'git rm .env' : .git .gitignore app.py

The file is gone. On a .env holding credentials that is a bad afternoon, so it is worth building the habit of typing --cached before you type the filename.

For a directory

$ git rm -r --cached build

-r for recursive, as with rm. Everything under it is untracked in one go and nothing is deleted from disk.

Re-applying .gitignore to the whole tree

When several files have crept in, do the lot:

$ git rm -r --cached .
$ git add .
$ git commit -m "apply .gitignore"

This untracks everything and then re-adds it — and the re-add does respect .gitignore, so anything matching a rule stays out. What is left staged is exactly the set of files that should have been ignored all along:

D  .env

Nothing is deleted from your working tree; only the index is rebuilt. Still, commit or stash your real work first, so the diff you are reading afterwards contains only this.

It is still in the history

This is the part that matters if the file held a secret:

$ git show HEAD~1:.env
SECRET=1

Untracking a file changes the future, not the past. Every commit that contained it still contains it, and anyone with a clone has it.

So if what leaked was a credential, rotate it. That is the fix. Rewriting history with git filter-repo or BFG removes it from the repository, which is worth doing for hygiene, but it does not un-leak anything — the old commits may already be in other people’s clones, in CI caches, in forks and in backups. Treat the rewrite as tidying up after the real remedy, not as the remedy.

About Netcup (advertisement)

The German host Netcup offers, among other things, affordable and powerful web hosting packages, KVM-based root servers and dedicated servers. With our voucher codes you can save even more (6€ off your first order, 30% off all KVM-based root servers, ...).