Great work!

XP to next level

BugEater
EN

.gitignore and .gitkeep: Keeping Junk and Secrets Out

Learning Objectives

By the end of this lesson you will be able to:

  • Write a .gitignore that covers a test-automation project
  • Use the pattern syntax that actually matters: /, *, **, !
  • Explain why ignoring an already-tracked file does nothing, and fix it
  • Commit an empty directory with .gitkeep

What .gitignore Actually Does

It is a list of patterns. Any untracked file matching one is hidden from git status and cannot be added by an ordinary git add ..

The critical word is untracked. A file Git is already tracking is completely unaffected — this is the single most common .gitignore confusion, and we will fix it below.

The file lives in the repository root, is committed like any other file, and applies to every directory beneath it. A subdirectory can have its own, which adds to the parent's rules.

The Patterns Worth Knowing

# A comment

node_modules/          # a directory, anywhere in the tree
*.log                  # any file with this extension, anywhere
/config.local.json     # only in the repository root — the leading slash anchors it
build/**/*.map         # ** matches across directory levels
!important.log         # ! un-ignores something an earlier rule caught

Four rules cover nearly everything:

  • Trailing / means "directory".
  • Leading / anchors the pattern to the repository root; without it, the pattern matches at any depth.
  • * matches within one path segment; ** matches across segments.
  • ! negates — but it cannot resurrect a file whose parent directory is ignored. Ignore logs/* and negate a file inside, not logs/ itself.

A Real Automation .gitignore

Test-automation projects generate more junk than almost anything else. This is a realistic starting point:

# Dependencies
node_modules/
.venv/

# Build output
dist/
build/
target/

# Test artefacts — regenerated on every run
screenshots/
videos/
allure-results/
test-results/
playwright-report/
*.har

# Logs
*.log
logs/

# Secrets and local config — NEVER commit these
.env
.env.*
!.env.example
credentials.json
*.pem

# Editor and OS noise
.idea/
.vscode/
.DS_Store

Two things to notice.

!.env.example un-ignores the template, so the team still knows which variables exist while the real values stay out. Committing .env.example and ignoring .env is the standard pattern, and it is worth adopting on day one.

Test artefacts are ignored as a category. A single Playwright run can leave hundreds of megabytes of video, and none of it belongs in history — it is output, not source.

The Trap: "I Added It and It's Still There"

You commit .env by accident. You notice, add it to .gitignore, and it still shows up in git status.

Because .gitignore only applies to untracked files, and this one is now tracked.

git rm --cached .env
git commit -m "chore: stop tracking .env"

--cached removes it from Git's tracking without deleting it from your disk. From the next commit onward it is untracked, so .gitignore finally applies.

For a directory:

git rm -r --cached node_modules/

The Part That Is Not Fixed

The file is out of future commits. It is still in every past commit, in every clone, forever.

For build output that is merely embarrassing, fine. For a secret, it is not. A committed credential must be treated as compromised: rotate it immediately. Removing it from history entirely means rewriting every commit that touched it (git filter-repo, or the platform's secret-removal tooling) and force-pushing, which invalidates every clone in the company — a coordinated operation, not a quick fix.

Rotate first, then decide whether the history rewrite is worth it. It usually is not, if the credential is already dead.

.gitkeep: Committing an Empty Directory

Git tracks files, not directories. A directory exists only because files in it do, so an empty screenshots/ folder cannot be committed — and your test run fails because the path does not exist.

The convention is a placeholder file:

mkdir -p test-results
touch test-results/.gitkeep
git add test-results/.gitkeep

.gitkeep is not a Git feature. It is an ordinary empty file, and the name is purely a convention meaning "this exists to hold the directory open". Some teams use .gitignore for the same job.

When the directory's contents are ignored, keep the placeholder visible:

test-results/*
!test-results/.gitkeep

The directory is committed, everything generated inside it is ignored.

Pro Tip: Check .gitignore into the repository on the very first commit of any automation project, before the first test run. Retrofitting one after node_modules and a week of screenshots are already in history is an afternoon of work that ten seconds at the start would have avoided.

Key Takeaways

  • .gitignore affects untracked files only
  • / anchors to the root, trailing / means directory, ** crosses levels, ! negates
  • Ignore build output, test artefacts, logs, secrets and editor noise; commit .env.example
  • An already-tracked file needs git rm --cached before the ignore rule takes effect
  • That does not remove it from past commits — treat a committed secret as compromised and rotate it
  • Git cannot track an empty directory; commit a .gitkeep placeholder
  • Add .gitignore on the first commit, not after the first mess

Quiz

You add .env to .gitignore but it still appears in git status. Why?

git rm --cached .env does what to the file on your disk?

After git rm --cached on a committed API key, what is the situation?

Why can't you commit an empty screenshots/ directory?