A commit is the unit of history. Six months from now, it is the commits that you (or a colleague) will use to figure out what changed and why. The difference between a history that helps and a history that is a dumping ground is craft: what to put into a commit, how to label it, and what to keep out of the repository entirely.
.gitignore crosses out .env and build/ while they are still in the working directory — Git simply does not show them. Out of what is left, git add picks into the index exactly the chunk that will go into the commit, and git commit turns the index into a new node of history. Two edits that happened to land in one file give two nodes — and each one can be reverted on its own.
Atomic commit: one change — one snapshot
A good commit is atomic: one logical change in its entirety. Fixed a bug — commit; reformatted code — a separate commit; added a feature — a third one. The bad extremes: a "dumpster commit" (bug + refactoring + new feature in one) and a "saved just in case" commit with half of an unfinished thought.
Why it matters: atomic commits can be reverted one by one, moved between branches, and understood during review. A dumpster — only as a whole.
Precision of the set is what the index provides:
git add src/OrderService.java # only the files you need
git add -p # interactively: piece by piece within a file!
git commit -m "fix: recalculate total when removing the last item"
git add -p is an underrated command: it shows changes in chunks and asks which ones to include. That is how one file with two edits becomes two clean commits.
The commit message: answering "why"
The first line is a short summary of up to ~70 characters with no trailing period: git log --oneline shows nothing but that line. The form varies — imperative (reset the total), a noun (total recalculation), a symptom (total was not reset); what matters is not which, but that the whole team writes the same way. What exactly changed is visible from the code; the message answers what the code does not contain: why.
- Bad:
fix,edits,wip,changed OrderService. - Good:
fix: reset cart total when the last item is removed.
Many teams use conventional commits prefixes (feat:, fix:, refactor:, docs:) — they make it easy to filter history and generate a changelog automatically. If more context is needed — a blank line after the summary and a paragraph of details.
Reading history: log, diff, blame
git log --oneline -15 # the last 15 commits, briefly
git log -p file.txt # history of a specific file with changes
git diff # what changed but is not yet in the index
git diff --staged # what is already in the index (will go into the commit)
git show abc123 # what a specific commit did
git blame OrderService.java # who changed each line and in which commit
git blame sounds accusatory, but it is a tool of archaeology, not of finding culprits: you found a strange line → blame → the commit → its message explains "why" → and if it doesn't, you know whom to ask. The "diff before commit" habit cuts off accidental changes: a second of review saves you from committed debug output.
.gitignore: what does not belong in history
Git offers to commit everything it sees — but not everything should end up in the repository:
# build artifacts — reproducible from the code
build/
# dependencies — restored from the lock file
node_modules/
# logs
*.log
# IDE settings
.idea/
# SECRETS — never in git!
.env
The comments sit on separate lines, and that is not a matter of taste. In .gitignore a # starts a comment only at the beginning of a line; write build/ # artifacts and Git will look for a folder whose name includes the spaces and the hash — the rule simply never fires, and build/ keeps landing in commits. To check what is actually ignored, run git status --ignored.
The .gitignore file in the project root lists such paths — Git stops noticing them. Two rules. First: generated and personal things are ignored, source and shared things are committed. Second, and most important: secrets (passwords, API keys) must never get into history — history is distributed and eternal, truly deleting from it is hard; a key that made it into a commit is considered compromised and must be rotated. On automatic protection against this — Gitleaks in the security series.
Fixing the last commit
Forgot a file or made a typo in the message — do not create a "fix the previous one" commit:
git add forgotten-file.txt
git commit --amend # replaces the last commit with a new one
One caution: --amend rewrites the commit — it is safe as long as the commit has not been sent to the server. The "don't rewrite what's been pushed" rule is covered in detail in the rebase article.
In short
- An atomic commit: one logical change.
git add -pbuilds a commit piece by piece. - The message answers "why": a short summary, with a paragraph of details when needed. feat/fix prefixes help with filtering.
- History is a tool: log for the timeline, diff before committing, blame for archaeology.
- .gitignore: build output, dependencies, IDE files — out; secrets — never in git, and if they got in — rotate them.
--amendfixes the last commit, as long as it has not been pushed.
What to read next
- Branches and merging — the next topic in the series.
- Undo and recovery — reset, revert, and reflog when something goes wrong.
- Rebase and cherry-pick — how to tidy a series of commits before colleagues see it.
- What is Git — if you missed the beginning.