datarekha

Ignoring files with .gitignore

Keep secrets, build artifacts, and node_modules out of your repository using .gitignore patterns and git rm --cached.

5 min read Beginner Git Lesson 6 of 15

What you'll learn

  • What .gitignore is and where it lives
  • How glob patterns, negation, and anchoring work in .gitignore
  • The crucial difference between untracked files and already-committed files

Before you start

The last lesson ended by naming the danger: git add . will sweep secrets, build artifacts, and OS cruft straight into history, where a leaked key lives forever. The fix is a single plain-text file that tells Git what to pretend it cannot see. Here is how it works — and the one gotcha that bites everyone who adds it too late.

Why a clean repository matters

When someone clones your repository they should get source code — not your Python bytecode cache, not a 200 MB node_modules folder, and definitely not your API keys. A repository bloated with generated or secret files is slower to clone, harder to review, and potentially dangerous to share.

Git’s answer is .gitignore: a plain-text file that tells Git which files and folders to pretend do not exist.


Where .gitignore lives

Place .gitignore in the root of your repository. Git reads it automatically — no configuration needed.

my-project/
├── .gitignore        ← the main one
├── src/
│   └── app.py
└── data/
    └── .gitignore    ← optional: rules for this subdirectory only

You can also place a .gitignore inside any subdirectory. Rules in a nested .gitignore apply only within that subdirectory. For most projects a single root-level file is all you need.


Pattern syntax

Each line in .gitignore is a glob pattern — a wildcard expression similar to what your shell uses for filename matching.

PatternWhat it ignores
*.logAny file ending in .log, anywhere in the tree
build/The entire build directory (trailing / means directory)
# commentNothing — lines starting with # are comments
!keep.logRe-includes keep.log even if a prior rule excluded it (negation)
/secret.txtOnly secret.txt at the repo root (leading / anchors to root)
**/logsA directory named logs anywhere in the tree (**/ means recursive)
doc/**/*.pdfAll .pdf files inside doc/ and any of its subdirectories

Glob means the pattern language where * matches any sequence of characters (except /), ? matches one character, and [abc] matches a set. It is not regular-expression syntax.


A realistic .gitignore

Here is a file you could drop into a Python project that also has a JavaScript build step:

# === Python ===
__pycache__/
*.py[cod]
*.pyo
.venv/
venv/
dist/
*.egg-info/

# === Secrets ===
.env
.env.*
!.env.example

# === Node / frontend ===
node_modules/
dist/
.next/
.nuxt/

# === OS files ===
.DS_Store
Thumbs.db

# === Editors ===
.vscode/
.idea/
*.swp

A few notes on the patterns above:

  • *.py[cod] uses a character class to match .pyc, .pyo, and .pyd in one rule.
  • .env.* covers .env.local, .env.production, etc. The !.env.example negation re-includes the example file so teammates can copy it as a starting point.
  • dist/ appears under both Python and Node because both ecosystems write output there. Duplicate rules are harmless.

The crucial gotcha: tracked vs. untracked

The practical order of operations is:

  1. Create .gitignore with your patterns before your first git add.
  2. If you already committed a file by accident, run git rm --cached and commit the removal.
  3. Rotate any leaked credentials regardless of step 2.

The filter in action

Working Directoryapp.py.envnode_modules/pycache/.gitignoredropped (.env, node_modules/, pycache/)What Git seesapp.py only
The .gitignore filter sits between your working directory and Git’s index. Files that match a pattern never reach git status or git add.

Ready-made templates

You rarely need to write a .gitignore from scratch. Two authoritative sources:

  • github/gitignore — the official collection at github.com/github/gitignore. Each file is named after a language or framework (Python.gitignore, Node.gitignore, etc.).
  • gitignore.io (also at toptal.com/developers/gitignore) — generates a combined file for multiple environments at once. Search “Python macOS VSCode” and download the result.

Copy the relevant template into your repo root, trim anything that does not apply, and add your own project-specific entries at the bottom.

In one breath

.gitignore is a plain-text file (usually at the repo root) listing glob patterns for files Git should pretend do not exist: *.log, build/, __pycache__/, .env. A trailing / means a directory, a leading / anchors to the root, **/ recurses, and a leading ! re-includes something a prior rule excluded — the last matching rule wins. The one gotcha that bites everyone: .gitignore only stops untracked files from being tracked. If a file was already committed, it stays tracked until you git rm --cached it, and even then it survives in old history — so a leaked secret must be rotated, not merely deleted. The right order is to write .gitignore before your first git add, and to start from a ready-made template (github/gitignore) rather than a blank file.

Practice

Before the quiz, work the gotcha: you clone a teammate’s repo and find .env — with live keys — has been tracked since the very first commit, even though .gitignore lists .env. Why does the ignore rule not apply here, which two commands stop Git tracking the file going forward, and why is that still not enough to make the leaked keys safe?

Quick check

0/3
Q1You add node_modules/ to .gitignore, but git status still shows node_modules/ as modified. What is the most likely cause?
Q2Your .gitignore contains the line *.log and later the line !important.log. What happens to important.log?
Q3A colleague accidentally committed .env with real AWS keys six months ago, then immediately ran git rm --cached .env and committed the removal. Are the keys still accessible?

A question to carry forward

That completes the fundamentals — you can create a repo, stage and commit deliberately, read the history, and keep junk out of it. But look back at everything you have done: it has all happened on a single straight line. One main, with HEAD marching forward, each commit stacked on the last. Real teams never work that way — and you already glimpsed the alternative in git log --graph --all, where a second line peeled off and later rejoined. That diverging line is a branch, the mechanism that lets ten people build ten features at once without ever overwriting each other. How a branch can be cheap enough that you make one for every small task — and what is really happening when two of them merge back together — is where the next chapter begins.

Sign in to track your progress

Completed lessons, your XP, level, and streak save to your account — it's free and takes a few seconds.

Related lessons

Explore further

Cheat sheets
Skip to content