Commits: the working folder, the staging area and history
How Git moves your changes from the working folder to the staging area to a commit, how to write commit messages people can use, keep secrets out with .gitignore, and read history with git log.
By the end of this lesson you'll be able to
0 of 6 completeThree places a change lives
Every change you make passes through three places. Once you can picture them, every Git command is just "move a change from here to there":
| Place | What's there | Moved in by |
|---|---|---|
| Working folder | your files as they are on disk, edits included | you, editing |
| Staging area (the index) | the exact version of each file for the next commit | git add |
Repository (.git) | every commit ever made, permanent | git commit |
git status tells you where everything is. Read it before every commit:
$ git status On branch main No commits yet Untracked files: (use "git add <file>..." to include in what will be committed) README.md app.py nothing added to commit but untracked files present (use "git add" to track)
git statusgitversion controlstatuswhat changed
Untracked files are ones Git has never been told about. They're never committed until you git add them.
The staging area
The staging area is why Git commits are clean: you choose exactly what goes into each commit, even when your working folder has five unrelated changes.
$ git add README.md $ git status --short # the compact form: two columns, staged | not staged A README.md ?? app.py $ git commit -m "Add README" $ git add app.py $ git commit -m "Add the app entry point"
git add README.mdgitversion controladdstage changes
git status --shortstatuswhat changed
git commit -m "Add README"commitsave staged changes
--short code | Means |
|---|---|
?? | untracked |
A | new file, staged |
M | modified, staged |
M | modified, not staged |
MM | staged, then changed again |
D | deleted, staged |
Now see a change before and after staging it:
$ echo "print('bye')" >> app.py $ git diff # working folder vs staging: not staged yet diff --git a/app.py b/app.py index 9f1b437..06e2325 100644 --- a/app.py +++ b/app.py @@ -1 +1,2 @@ print('hi') +print('bye') $ git add app.py $ git diff # nothing: everything is staged $ git diff --staged # staging vs last commit: what you'll commit $ git restore --staged app.py # unstage it (the edit stays in your file) $ git restore app.py # throw the edit away (can't be undone)
echo "print('bye')" >> app.pyechoprint text>>append output to a file
git diffgitversion control
git add app.pyaddstage changes
git add -p goes further: it shows each change in a file and asks y/n for each one, so one file
with two unrelated fixes becomes two clean commits.
Commits and messages
A commit is a snapshot of the staged files, plus your name, the time, a message and a link to the
commit before it. Each one has an ID: a 40-character hash such as
6e0c7057d4ddf7b9618bef0769e17e331429ebfa, usually shortened to the first seven (6e0c705).
The message is for the person reading history a year from now, often you, trying to find why something changed:
Fix crash when the cart is empty Checkout divided by the item count to show an average price, which failed with zero items. Show the total only when the cart is empty. Fixes #142
| Rule | Why |
|---|---|
| Subject in the imperative, about 50 characters: "Fix …", "Add …", "Remove …" | reads as "this commit will fix …", and fits in one-line views |
| Blank line, then a body saying why | the diff already shows what changed |
| One logical change per commit | easy to review, revert or cherry-pick |
Reference the issue: Fixes #142 | GitHub links it and closes the issue on merge |
Many teams use Conventional Commits (feat: add coupon codes, fix: empty cart crash), which
tools read to generate changelogs and version numbers (the Tags and releases lesson). git commit
without -m opens your editor for a message with a body.
.gitignore: what Git should never see
Some files must never be committed: secrets, dependencies you can reinstall, build output, editor
clutter. List them in .gitignore at the top of the repository and commit that file:
$ git status --short ?? .env ?? __pycache__/ $ printf '.env\n__pycache__/\n*.log\n' > .gitignore $ git status --short # .env and __pycache__ have disappeared ?? .gitignore $ git check-ignore -v .env # which rule ignores it? .gitignore:1:.env .env
git status --shortgitversion controlstatuswhat changed
printf '.env\n__pycache__/\n*.log\n' > .gitignore>write output to a file, replacing it
# .gitignore: one pattern per line; comments go on their own line # one file, anywhere in the repository .env # every .log file *.log # a folder (the trailing slash means folders only) node_modules/ # only build at the top level /build # an exception: don't ignore this one !keep.log
A # only starts a comment at the beginning of a line: .env # secret would be read as one long
file name. Start from GitHub's template for your language (linked below) instead of writing one from memory.
Reading history with git log
$ git log --oneline 6e0c705 Add the app entry point fffd711 Add README $ git log --stat -1 # the last commit and the files it changed commit 6e0c7057d4ddf7b9618bef0769e17e331429ebfa Author: Rajeev Kumar <rajeev@example.com> Date: Tue Oct 13 06:54:35 2026 +0000 Add the app entry point app.py | 1 + 1 file changed, 1 insertion(+) $ git log -p -1 # the full patch of the last commit $ git show fffd711 # any one commit $ git log --oneline -- app.py # only commits that touched app.py $ git log --oneline --graph --all # every branch, drawn as a graph
git log --onelinegitversion controlloghistory
The History and diffs lesson goes much further: searching history, comparing branches and finding the commit that broke something.
Start a project repository the right way
Create a repository for a small app that reads a secret from .env. Ignore the secret and generated files before the first commit, make three focused commits with good messages, and prove the secret was never committed.
A repository with 3 commits and no .env in historyReveal one safe solution
$ mkdir ~/projects/shop && cd ~/projects/shop && git init $ printf '.env\n__pycache__/\n*.log\n' > .gitignore $ git add .gitignore && git commit -m "Ignore secrets and generated files" $ echo "API_KEY=change-me" > .env $ echo "# Shop" > README.md && git add README.md && git commit -m "Add README" $ printf 'import os\nprint(os.environ.get("API_KEY", "no key"))\n' > app.py $ git add app.py && git diff --staged --stat && git commit -m "Add the app entry point" app.py | 2 ++ 1 file changed, 2 insertions(+) $ git log --oneline dad9c23 Add the app entry point 2d89f66 Add README 1221cd2 Ignore secrets and generated files $ git ls-files # everything Git tracks .gitignore README.md app.py $ git log --all --oneline -- .env # any commit that ever contained .env? (no output = none)
mkdir ~/projects/shop && cd ~/projects/shop && git initmkdirmake a directory&&run the next command only if this one workedcdchange directorygitversion controlinitcreate a repository here
printf '.env\n__pycache__/\n*.log\n' > .gitignore>write output to a file, replacing it
git add .gitignore && git commit -m "Ignore secrets and generated files"addstage changescommitsave staged changes
echo "API_KEY=change-me" > .envechoprint text
git log --onelineloghistory
Committing .gitignore first means no later git add . can sweep .env in. Your commit IDs will be
different: they depend on your name, the time and the content.
Try it yourself
0 of 6 steps doneRun each task in your own account or terminal, then tick it off.
Quick check
You changed app.py and ran git add app.py, then changed it again. What will git commit save?
Official reading for this lesson
Or get every quiz answer right and it completes itself.