Good commits: small, intentional and well described
The interactive lesson needs JavaScript. The text below is the complete lesson.
After this lesson you can
- split mixed changes into commits with one purpose each, also inside one file with
git add -papply - write a commit message with a short imperative subject line and a body that explains why apply
- use
.gitignoreto keep build output, logs and secrets out of history, and explain its limits apply
Understand: One commit, one purpose
Imagine a commit called Fix stuff. It fixes a typo, adds a new week to the plan and includes a log file by accident. In a month nobody knows what it was for. Undoing just one part is also awkward.
Here is the same work as two small commits, and the log file is not included at all. Each commit has one purpose. That makes them easier to read, easier to undo and easier to search when a bug shows up.
Look at the messages. The subject line is short, ideally under fifty characters, and written as a command: Add exam week, not Added stuff. It completes the sentence “If applied, this commit will”. The why goes in a body after a blank line.
But what if both changes are in the same file? Let us rewind to before the commits. git add -p shows the changes in blocks, called hunks, and asks about each one. Here only the typo fix is staged. The new week waits in the working tree.
Some files should never enter history: build output, logs and secrets such as passwords. The file .gitignore lists patterns, for example *.log. Git stops showing untracked files that match, and git add . skips them. You commit .gitignore itself like any other file.
Ignore has two limits. It does not affect files Git already tracks. And it is not a security mechanism: once a password is in a commit that has been shared, it lives in history. Then the password must be changed.
Watch: Split a mixed change
We have been working on a course plan for a while. The status shows two things. plan.txt is modified. debug.log is untracked: a log the program wrote, which does not belong in history.
$ git status --short
The diff has two blocks that start with two at signs. Each block is a hunk. The first fixes a typo in week two. The second adds week eleven. Two different purposes, so two commits.
$ git diff
git add -p asks hunk by hunk. We answer y to the typo fix and n to the new week. printf just types the answers for us, so the demo does not stall. Now plan.txt differs between the staging area and the working tree.
$ printf 'y\nn\n' | git add -p plan.txt
Before committing, we check the draft. git diff --staged shows only the typo fix. That is exactly what should go in.
$ git diff --staged
The subject line is short, imperative, and says what the commit does. The second -m becomes the body, which explains why. Week eleven is still only in the working tree.
$ git commit -m "Fix typo in week 2" -m "The misspelling made the plan hard to search."
Now the log file. We create a .gitignore with the pattern *.log. That means every file that ends in .log, in every folder.
$ echo "*.log" > .gitignore
What do you think the status shows now?
$ git status --short
Tasks
- Read the changes that are not staged.
- Stage only the typo fix in
plan.txt, not the new week. - Commit the typo fix with a short subject line and a body that explains why.
- Make Git ignore all
.logfiles. - Check the status again.
Practise: Transfer: tidy a recipe project
A different project: a recipe, a local file with secrets, a build folder and a log file that was committed by mistake. Look at git status first.
Tasks
.envholds secrets, andbuild/is regenerated by the build tool. Make Git stop suggesting both, and commit the rule in a commit of its own. The secrets and build files must not enter history.recipe.txthas two unrelated changes: a spelling fix and a new serving tip. Commit only the fix. Give the commit a subject line of at most 50 characters and a body that explains why.app.logwas committed by mistake, and Git keeps showing changes in it. Stop tracking the file and make Git ignore it, but keep it on disk. Save this in a commit.
Check yourself: Recall it
Which subject line is best?
- Added password reset link to login page
- Add password reset link to login page
- Add reset link because users could not find it
config.env with a password was committed and pushed last week. Today you add config.env to .gitignore. What happens?
- Git removes the file from history
- Nothing for that file: Git still tracks it, and the password stays in history
- The file disappears from GitHub on the next push
In the same file you fixed a bug and reformatted a function unrelated to the bug. How should you commit?
- One commit, since it is one file
- Two commits: stage the bug fix with
git add -p, commit, then commit the formatting - Wait and commit everything together at the end of the day
Review from the lesson on the three areas: you have staged one hunk in plan.txt. Then you fix another typo in the same file and run git commit -m "Fix typo" without staging again. What does git status --short show for plan.txt afterwards?
- Nothing, because the commit took the whole file
Min the first column: the change is staged and waitingMin the second column: modified in the working tree, not staged
Common misconceptions
- “A commit is one file, or one day of work.” A commit is one purpose. It can touch many files, or just part of one.
- “
.gitignorehides files that are already committed, and protects secrets.” Ignore only applies to untracked files. A secret that has been pushed must be replaced.
Read more
What makes a commit good?
- One purpose. A commit that does one thing is easy to read in a code review, easy to undo with
git revertand easy to find again when you hunt for the commit that introduced a bug. - Complete. The opposite mistake is splitting too much. A commit that leaves the project broken, for example a function without the call it needs, makes history harder to use. Each commit should ideally build and pass the tests.
- A steady rhythm: edit, read
git diff, stage what belongs together, readgit diff --staged, commit, and run the tests.
git add . is quick but imprecise: it takes everything under the folder, including files you forgot existed. Name the files, or use git add -p.
The commit message
Fix typo in week 2
The misspelling made the plan hard to search for
"staging". No other weeks are affected.
- The subject line is short, ideally at most 50 characters, imperative and without a full stop. It completes the sentence "If applied, this commit will …". Write
Add exam week, notAdded stuff. - A blank line separates subject and body. Tools such as
git log --onelineshow only the subject line. - The body explains why and any consequences. What changed is already in the diff. Wrap lines at about 72 characters.
With -m twice, the first value becomes the subject line and the second a paragraph of its own:
$ git commit -m "Stop tracking app.log" -m "The log is rewritten on every run and only adds noise to diffs."
$ git log -1
commit b33e60b41b7756c5b4e28b6a7ff2abfc63dbf542
Author: Student <student@example.invalid>
Date: Tue Oct 13 09:10:51 2026 +0200
Stop tracking app.log
The log is rewritten on every run and only adds noise to diffs.
Without -m, git commit opens the editor set in core.editor. Lines starting with # are removed. Save an empty message and the commit is aborted. git commit -v also shows the diff at the bottom of the editor, so you see what you are describing.
git add -p in detail
git add -p splits the changes into hunks and asks about each one:
$ git add -p plan.txt
diff --git a/plan.txt b/plan.txt
index f757011..e85757e 100644
--- a/plan.txt
+++ b/plan.txt
@@ -1,6 +1,6 @@
Course plan
Week 1: snapshots
-Week 2: stagign
+Week 2: staging
Week 3: history
Week 4: branches
Week 5: merging
(1/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?
The list of letters in the brackets varies a little between Git versions. These answers exist in all of them:
| Answer | Meaning |
|---|---|
y / n |
Stage this hunk / skip it |
a / d |
Stage the remaining hunks in the file / skip the rest |
s |
Split the hunk into smaller pieces, when there are unchanged lines between the changes |
e |
Edit the hunk by hand |
q |
Quit. What you answered yes to stays staged |
? |
Show help |
Afterwards git status --short shows the file as MM: one part is staged, the rest waits. Always check with git diff --staged before committing. If you staged one hunk too many, git restore --staged -p takes it out again, with the same questions.
What git commit -a includes
-a stages changes and deletions in files Git already tracks, and commits. New files are not included, and you skip reviewing the draft:
$ echo tmp > notes.txt
$ git commit -am "Add notes"
On branch main
Untracked files:
(use "git add <file>..." to include in what will be committed)
notes.txt
nothing added to commit but untracked files present (use "git add" to track)
.gitignore patterns
| Pattern | Matches |
|---|---|
*.log |
Every file ending in .log, in every folder |
build/ |
Every folder called build, with all its content |
/todo.txt |
Only todo.txt in the root folder |
!keep.log |
Exception: keep.log should show after all |
docs/**/*.pdf |
PDF files at any depth below docs |
# text |
A comment |
Ignored files disappear from git status. --ignored shows them with !!, and git check-ignore -v tells you which rule matched. A rule starting with ! means the file is not ignored after all:
$ git status --short --ignored
M plan.txt
?? .gitignore
?? keep.log
!! build/
!! debug.log
$ git check-ignore -v debug.log build/a.js keep.log
.gitignore:2:*.log debug.log
.gitignore:3:build/ build/a.js
.gitignore:4:!keep.log keep.log
If you try to stage an ignored file, Git tells you:
$ git add other.log
The following paths are ignored by one of your .gitignore files:
other.log
hint: Use -f if you really want to add them.
.gitignoreis committed and shared with everyone. Local exceptions that only apply to you go in.git/info/exclude. Files your editor or operating system creates, such as.DS_Storeor.idea/, fit best in a global file set withcore.excludesFile.- Git tracks only files, not empty folders. To keep an empty folder, put a small file in it, often called
.gitkeep.
When the file is already tracked
Ignore applies only to untracked files. If app.log is already committed, a new rule changes nothing: Git still shows its changes. This is how you stop tracking it without deleting it from disk:
$ git rm --cached app.log
rm 'app.log'
$ echo "app.log" >> .gitignore
$ git add .gitignore
$ git commit -m "Stop tracking app.log"
git status --short shows D app.log before the commit: the file leaves the snapshot but stays on your disk. When others bring the commit in with a merge or pull, Git deletes the file from their working tree.
Secrets are a separate problem. Old commits still contain the file, and everyone who fetched them has a copy. A password or key that has been pushed must be replaced. A new commit that deletes the file is not enough. Commit a template such as
.env.examplewithout real values instead.
Further reading
- git commit, the "Discussion" section on message format
- git add, interactive mode
- gitignore, git check-ignore and git rm
Per-Arne's Git Good!