Files that must never be committed
Run a project for a day and junk appears. Log files, build output, editor droppings, dependency folders like node_modules holding 40,000 files that any machine can regenerate in a minute.
Worst of all are secrets, files like .env holding passwords and API keys. Committing a secret publishes it to everyone with repository access, permanently, because it stays in history even after a later commit deletes the file.
The fix is a file named .gitignore at the repository root. Each line is a pattern, and files matching a pattern stop appearing as untracked.
# dependencies and build output
node_modules/
build/
# logs, any file ending in .log
*.log
# secrets
.env
# OS junk
.DS_StoreThe pattern rules are short. A bare name matches that file anywhere in the project, a trailing / means a whole folder, * is the wildcard from the terminal course so *.log matches any name ending in .log, and a line starting with # is a comment.
Commit the .gitignore itself. It is shared team policy rather than personal configuration, so everyone who clones the project inherits the same rules and nobody accidentally commits the build folder.
How the wildcard matches
The * in .gitignore works like the shell globs from the terminal course, so ls can demonstrate exactly which files a pattern would catch.
touch app.py notes.txt debug.log error.log echo "Files matched by *.log:" ls *.log echo "Files matched by *.txt:" ls *.txt
Output
Files matched by *.log: debug.log error.log Files matched by *.txt: notes.txt
The touch created four empty files, and each ls listed only the ones its glob matched.
*.log means any characters followed by .log, which is why app.py and notes.txt are absent from the first list. That is the same reasoning .gitignore applies, so reading a pattern as a question about file endings is usually enough to predict its effect.
The gotcha: .gitignore cannot un-track a file
.gitignore works at one precise moment. It stops files from ever becoming tracked, which means it has no authority over a file that is already committed.
A file past that gate stays tracked, pattern or no pattern, and the reason is protective: silently ignoring an already-tracked file would discard changes teammates might be relying on.
So when you add *.log to .gitignore and debug.log, committed last week, keeps showing up in git status, the fix is to remove it from tracking while leaving it on disk.
$ git rm --cached debug.log
$ git commit -m "Stop tracking debug.log"--cached is the crucial flag. It removes the file from the staging area and therefore from the next commit's contents, without touching your folder, while plain git rm deletes it from both. From that commit onward the .gitignore pattern applies as expected.
Remember the earlier warning here. If the file held a secret, untracking it does nothing about the old commits that still contain it, so the only real remedy is to rotate the secret.
README.md, the front page
GitHub renders a file named README.md from the repository root directly on the project's page, which makes it the first thing every visitor reads. It is written in Markdown, the same # headings, **bold**, code fences, and lists that appear throughout this course.
A solid README answers four questions, in this order.
- What is this? One or two sentences, written for someone who arrived from a search result.
- How do I run it? The exact install and start commands, copy-pasteable.
- How do I use it? One minimal example.
- How do I contribute? Where pull requests and issues go, from lessons 8-1 and 8-2.
For your portfolio these carry real weight. Recruiters and interviewers do open your repositories, and a clear README beside a tidy git log is direct evidence that you work like a professional rather than a claim that you do.
Deleting the file in a new commit is not enough because the key still exists in the old commit, so anyone with the history can read it.
Commits are permanent snapshots, as lesson 1-2 established, and a later commit adds to history rather than editing it. Anyone who clones the repository receives every commit, including the one holding the key, so a deletion changes only the current state of the project.
The professional response is to treat the key as leaked, which it is, and revoke and rotate it immediately. Adding the file to .gitignore afterwards prevents a repeat, but it does nothing about the exposure that already happened.
The command is git rm --cached debug.log.
It removes the file from the staging area and from the next commit, so Git stops tracking it, while leaving the copy in your folder untouched. Commit that change and the .gitignore pattern takes over from then on.
The flag is what makes the difference, and the name points at the staging area under its older name, the cache. Plain git rm debug.log would untrack the file and delete it from disk, which is a reasonable command in its own right but not what you want when the file is a log you still expect the program to write to.
Matching a different extension
The same demonstration with a mixed set of files, this time asking which names a *.py pattern catches.
touch main.py helper.py secret.env output.log echo "Matched by *.py:" ls *.py
Output
Matched by *.py: helper.py main.py
Two details are worth noting. touch accepts several names at once, so one command created all four files, and ls *.py listed only the names ending in .py, alphabetically, which is why helper.py prints before main.py.
secret.env and output.log were untouched by this pattern, which is exactly why a real .gitignore needs a separate line for each kind of file it means to exclude.
The file is .gitignore, kept at the repository root.
It holds one pattern per line, whether a plain name, a folder with a trailing /, or a wildcard such as *.log, and it keeps junk and secrets out of git status and out of commits.
Commit the file itself so the whole team shares the same rules. The leading dot makes it hidden in ordinary directory listings, in the same way as .git, so ls -a is how you confirm it exists.