How the matching rules actually work
A .gitignore pattern without a slash matches at any depth, so build ignores a build directory anywhere in the tree. A pattern containing a slash anywhere except at the end is anchored to the directory holding the file, so src/build matches only that one path. A trailing slash restricts the pattern to directories, which is how you ignore a cache/ folder without also ignoring a file named cache.
Order matters, because later rules override earlier ones. A negation with ! re-includes something a previous pattern excluded, which is how you ignore a whole directory except one file. The catch is that Git will not descend into an excluded directory at all, so build/ followed by !build/keep.txt does not work — the negation is never reached. You must exclude the contents rather than the directory: build/* then !build/keep.txt.
Two asterisks match across directory separators, so **/logs matches at any depth and logs/** matches everything beneath. A single asterisk stops at a slash. This distinction is the source of most patterns that appear correct and quietly match nothing.
Ignoring does not untrack
The rule that surprises people most is that .gitignore only affects files Git is not already tracking. Adding a pattern for a file that has already been committed changes nothing — Git keeps tracking it, and every future modification still shows up.
Removing it requires git rm --cached <file>, which unstages the file while leaving it on disk, followed by a commit. For a directory, add -r. Until you do this, a committed .env file remains fully tracked no matter what the ignore file says.
More importantly, removing a file from tracking does not remove it from history. A secret committed once remains in every clone of the repository and is recoverable from any past commit, so deleting it later is not remediation. The only correct response to a committed credential is to rotate it — treat it as disclosed the moment it was pushed, and rewrite history separately if the repository is private and the exposure window was short.
What belongs in the file
The useful categories are consistent across languages. Dependencies that a package manager can reinstall — node_modules, virtual environments, vendored packages — should never be committed, since the lockfile records exactly what to fetch. Build output and compiled artefacts are derived from source and belong out of the repository. Caches and temporary files from test runners, bundlers and type checkers likewise.
Anything containing a secret must be excluded and, ideally, made hard to add accidentally: commit a .env.example with the keys and no values, so the shape is documented while the real file stays out.
The one genuinely contested category is editor and operating system files — .DS_Store, editor directories, swap files. The argument for keeping them out of the project file is that they reflect a personal choice of tooling that other contributors do not share; the cleaner approach is a global ignore file configured with core.excludesFile, which applies across every repository you work in without imposing your setup on anyone else.