The commit contains what you staged, not what you have
git add takes a copy. Anything you type afterwards is in the working tree and nowhere else until you add it again. This is not a flaw — it is what lets you keep editing while a commit is being assembled — but it catches everyone once.
echo "draft" > notes.md
git add notes.md # the index now holds "draft"
echo "final" > notes.md # the working tree now says "final"
git commit -m "Notes" # the commit contains "draft"
git status # notes.md: modified, not staged
Staging part of a file
One afternoon usually contains three commits' worth of work. git add -p is how you separate them afterwards, and it doubles as a review: you read every line you are about to commit, which catches debug logging more reliably than any hook.
git add -p price-list.md
# for each hunk:
# y stage it n skip it
# s split it into smaller hunks
# e edit the hunk by hand
# q stop
Taking things back out
git restore --staged notes.md # unstage: index ← HEAD. your edits are kept
git restore notes.md # discard edits: working tree ← index. NOT recoverable
git restore --source=HEAD~2 notes.md # bring back an older version, into both
# the older spellings, still everywhere
git reset HEAD notes.md # = restore --staged
git checkout -- notes.md # = restore
git restore <file> overwrites the file with the staged version. That edit was never committed, so git cannot get it back. It is the only common git command that destroys work outright.
.gitignore
A pattern with a trailing slash matches directories; does not cross a slash, * does; ! un-ignores. The file belongs in the repository — the whole team needs the same rules.
node_modules/
dist/
.env
.env.*
*.log
.DS_Store
coverage/
!dist/.gitkeep # an exception to the rule above
If a file is already tracked, adding it to .gitignore changes nothing. You need git rm --cached .env and a commit — and if the secret was ever pushed, it is in the history for good: rotate it, do not just delete it.
Empty directories, and the file that is not code
Git tracks content, so an empty directory cannot be represented at all — hence the .gitkeep convention, which is just an empty file nobody reads. Large binaries (video, design files, datasets) bloat every clone forever; that is what Git LFS exists for, and it is worth setting up before the file goes in, not after.