What Belongs in Version Control vs What Should Never Be Committed
A clean repository contains the source and configuration needed to build and run the project, plus documentation that helps others understand it. It should not contain files that are generated, machine-specific, or sensitive. The goal of .gitignore is to prevent accidental commits of “noise” and secrets.
Good candidates to commit
- Source code:
src/,lib/, application modules, scripts. - Build configuration:
package.json,pyproject.toml,pom.xml,build.gradle, tool configs. - Lockfiles (usually):
package-lock.json,poetry.lock,requirements.txt(depending on your workflow). - Documentation:
README.md,docs/, examples. - Small, deterministic assets needed at runtime: templates, migrations, small static files (not large generated bundles).
Should be ignored (typical)
- Build artifacts:
dist/,build/, compiled binaries, minified bundles produced by your build. - Dependencies installed locally:
node_modules/, Python virtual environments (.venv/), Gradle caches. - Logs and runtime output:
*.log,coverage/, temp files. - OS and editor files:
.DS_Store,Thumbs.db,.idea/,.vscode/(often), swap files. - Secrets:
.env, API keys, private certificates, tokens, credentials JSON.
Never commit (even “temporarily”)
- Credentials: passwords, tokens, API keys, cloud credentials, SSH private keys.
- Private keys and certificates:
*.pem,id_rsa, signing keys. - Production data dumps: database exports, user data, proprietary datasets.
If a secret is committed, removing it from the working tree is not enough; it remains in history. The best strategy is prevention: ignore it and use safe patterns like .env.example (covered below).
Creating and Testing .gitignore Patterns
.gitignore is a plain text file placed at the repository root (and optionally in subdirectories). Each line is a pattern that tells Git which untracked files to ignore. Ignoring affects what shows up as “untracked” and what can be accidentally added.
Step-by-step: create a baseline .gitignore
1) Create the file at the repo root:
touch .gitignore2) Add common entries for your project. Example for a small web app:
Continue in our app.
You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.
Or continue reading below...Download the app
# Dependencies and environments
node_modules/
.venv/
# Build output
dist/
build/
# Logs
*.log
# OS files
.DS_Store
Thumbs.db
# Editor caches
.vscode/
.idea/
# Secrets
.env3) Verify what is currently untracked and whether it is now ignored:
git statusWildcards and common pattern rules
- Match by extension:
*.logignores any file ending in.log. - Match anywhere:
__pycache__/ignores directories named__pycache__wherever they appear. - Root-only patterns: prefix with
/to match only at the repository root. Example:/dist/ignores only rootdist, notsrc/dist/. - Directory rules: a trailing slash means “directory”. Example:
build/ignores the directory and everything under it. - Single-character wildcard:
file?.txtmatchesfile1.txtbut notfile10.txt. - Character ranges:
file[0-9].txtmatchesfile7.txt.
Negation with ! (ignore everything except…)
Negation lets you re-include a file that would otherwise be ignored. This is useful when you want to ignore a whole directory but keep a placeholder or a specific config file.
Example: ignore all files in logs/ but keep a .gitkeep placeholder:
logs/
!logs/.gitkeepExample: ignore all .env files but keep an example template:
.env
!.env.exampleImportant: if you ignore a directory (e.g., logs/), Git may not “see” files inside it to re-include unless the parent path is not fully excluded. A common workaround is to ignore contents instead of the directory itself:
logs/*
!logs/.gitkeepTesting patterns before you add files
Use Git’s pattern debugging to confirm whether a specific path is ignored and which rule caused it:
git check-ignore -v path/to/fileIf the file is ignored, the output shows the matching .gitignore line and file location. If there is no output, the file is not ignored.
Ignored vs Already-Tracked Files: The Key Difference
.gitignore only prevents untracked files from being added accidentally. If a file is already tracked (it has been committed before), adding it to .gitignore does not stop Git from tracking changes to it.
How to tell which case you are in
- If
git statusshows a file under “Untracked files”, it can be ignored by.gitignore. - If
git statusshows it under “Changes not staged for commit” or “Changes to be committed”, it is already tracked.
Practical example: you accidentally committed dist/ once. Later you add dist/ to .gitignore, but changes in dist/ still appear. That’s because the directory is tracked in history.
Removing Tracked Artifacts Safely with git rm --cached
To stop tracking a file while keeping it on disk, remove it from the index (staging area) using --cached. This is the standard fix for “I committed build output or a secret file by mistake, and now I want Git to stop tracking it.”
Step-by-step: stop tracking a file but keep it locally
1) Add the correct ignore rule first (so it won’t come back as untracked):
# .gitignore
.env2) Remove it from tracking (but keep the file in your working directory):
git rm --cached .env3) Confirm the change:
git status4) Commit the removal and the ignore rule together:
git add .gitignore
git commit -m "Stop tracking .env and ignore it"Step-by-step: remove a tracked directory of artifacts
Example: dist/ is tracked but should be ignored.
# 1) Add ignore rule
printf "/dist/\n" >> .gitignore
# 2) Remove from index recursively
git rm -r --cached dist/
# 3) Commit
git add .gitignore
git commit -m "Remove dist from tracking and ignore build output"Note: if the artifacts include sensitive data that has already been committed, git rm --cached prevents future commits but does not erase history. Treat that as a security incident: rotate credentials and consider history rewriting only if you fully understand the impact on collaborators.
Repository Hygiene: Secrets, .env.example, and Verifying What Will Be Committed
Use .env.example to document required environment variables
A common small-project pattern is:
.env: real secrets and machine-specific values (ignored)..env.example: a committed template showing required keys without real values.
Example .gitignore rules:
.env
!.env.exampleExample .env.example content:
# Copy to .env and fill in real values
DATABASE_URL=
API_KEY=
PORT=3000This keeps onboarding simple while preventing accidental credential commits.
Avoid committing credentials in any form
- Do not hardcode tokens in source files.
- Do not commit “temporary” JSON credentials (cloud service accounts, OAuth client secrets).
- Do not commit private keys, even in test folders.
If you need non-secret defaults, keep them in a committed config file (for example config/default.json) and load secrets from environment variables or an ignored local override file (for example config/local.json ignored).
Verify what will be committed: git status and git diff --staged
Before every commit, check two things: what is staged, and what content is staged.
1) See what is staged vs unstaged:
git status2) Inspect the exact staged changes (this is where secrets often get caught):
git diff --stagedPractical habit: if you ever see a token-like string, a private key header, or a password in git diff --staged, unstage it immediately and fix the source (move it to .env, config override, or secret manager).
Checklist: Common Ignore Rules for Small-Project Stacks
Node.js (npm/yarn/pnpm)
node_modules/dist/,build/, framework outputs (for example.next/,.nuxt/).envand related:.env.*(keep.env.example)- Logs:
npm-debug.log*,yarn-debug.log*,yarn-error.log*,*.log - Coverage:
coverage/
Python
- Virtual environments:
.venv/,venv/ - Bytecode and caches:
__pycache__/,*.pyc - Build artifacts:
build/,dist/,*.egg-info/ - Test/coverage:
.pytest_cache/,.coverage,htmlcov/ - Environment files:
.env(keep.env.example)
Java (Maven/Gradle)
- Maven:
target/ - Gradle:
build/,.gradle/ - IDEs:
.idea/,*.iml,.classpath,.project,.settings/ - Logs:
*.log - Environment files:
.env(keep.env.example)
Validate Ignore Rules Before the First Commit
Step-by-step pre-commit validation routine
1) Create .gitignore early, before installing dependencies or running builds.
2) Generate typical local artifacts once (install dependencies, run tests/build) so you can see what appears.
3) Check what Git thinks is untracked:
git status4) For any suspicious file or directory, confirm whether it is ignored and why:
git check-ignore -v path/to/suspicious-file5) Stage intentionally, then inspect staged content:
git add -A
git status
git diff --staged6) If something should not be committed, fix it now:
- Add/adjust
.gitignorepatterns. - If it was accidentally staged, unstage it (for example
git restore --staged path). - If it was already tracked, remove it safely with
git rm --cached(file) orgit rm -r --cached(directory), then commit the change.